diff --git a/src/components/layout/ChatArea.tsx b/src/components/layout/ChatArea.tsx index bfd56872..2441d61d 100644 --- a/src/components/layout/ChatArea.tsx +++ b/src/components/layout/ChatArea.tsx @@ -17,6 +17,7 @@ import { isScrolledToBottom } from "../../hooks/useScrollToBottom"; import { useTabCompletion } from "../../hooks/useTabCompletion"; import { useTypingNotification } from "../../hooks/useTypingNotification"; import { waitForAuthToken } from "../../lib/authToken"; +import { getClientCommands } from "../../lib/clientCommands"; import { useEmojiResolver } from "../../lib/customEmoji"; import { emojiClickValue, @@ -37,11 +38,14 @@ import { } from "../../lib/messageFormatter"; import { isMobileDevice, isTauriMobile } from "../../lib/platformUtils"; import useStore from "../../store"; -import type { Message as MessageType, User } from "../../types"; +import { queryUncachedBotsInChannel } from "../../store/handlers/pushbot"; +import type { BotCommand, Message as MessageType, User } from "../../types"; import { MessageItem } from "../message/MessageItem"; import { MessageReply } from "../message/MessageReply"; import AutocompleteDropdown from "../ui/AutocompleteDropdown"; import BlankPage from "../ui/BlankPage"; +import BotsModal from "../ui/BotsModal"; +import { BotToolsTray } from "../ui/BotToolsTray"; import ChannelSettingsModal from "../ui/ChannelSettingsModal"; import ColorPicker from "../ui/ColorPicker"; import EmojiAutocompleteDropdown from "../ui/EmojiAutocompleteDropdown"; @@ -56,10 +60,13 @@ import { MiniMediaPlayer } from "../ui/MiniMediaPlayer"; import ModerationModal, { type ModerationAction } from "../ui/ModerationModal"; import ReactionModal from "../ui/ReactionModal"; import { ReactionPopover } from "../ui/ReactionPopover"; +import { SlashCommandParamModal } from "../ui/SlashCommandParamModal"; import { getActiveSlashQuery, SlashCommandPopover, + type SlashSuggestion, } from "../ui/SlashCommandPopover"; +import { SlashParamHint } from "../ui/SlashParamHint"; import { TextArea } from "../ui/TextInput"; import { TopicMediaStrip } from "../ui/TopicMediaStrip"; import { @@ -141,6 +148,10 @@ export const ChatArea: React.FC<{ // with "/" and we're still completing the command name -- otherwise // typing wouldn't trigger a re-render for this popover at all. const [slashInputValue, setSlashInputValue] = useState(""); + const [paramModal, setParamModal] = useState<{ + botNick: string; + command: BotCommand; + } | null>(null); const [isEmojiSelectorOpen, setIsEmojiSelectorOpen] = useState(false); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [selectedColor, setSelectedColor] = useState(null); @@ -224,6 +235,7 @@ export const ChatArea: React.FC<{ useState(false); const [userProfileModalOpen, setUserProfileModalOpen] = useState(false); const [inviteUserModalOpen, setInviteUserModalOpen] = useState(false); + const [botsModalOpen, setBotsModalOpen] = useState(false); const [selectedProfileUsername, setSelectedProfileUsername] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const inputRef = useRef(null); @@ -1438,6 +1450,13 @@ export const ChatArea: React.FC<{ getActiveSlashQuery(newText, newCursorPosition) !== null; if (slashActive) { setSlashInputValue(newText); + // Lazy bot-cmds discovery: the first time the user starts a + // slash command in this channel, query any +B users we don't + // already have schemas for so the popover updates as soon as + // the response arrives. + if (!slashInputValue && selectedServerId && selectedChannel) { + queryUncachedBotsInChannel(selectedServerId, selectedChannel.name); + } } else if (slashInputValue !== "") { setSlashInputValue(""); } @@ -1988,6 +2007,7 @@ export const ChatArea: React.FC<{ onToggleNotificationVolume={handleToggleNotificationVolume} onOpenChannelSettings={() => setChannelSettingsModalOpen(true)} onOpenInviteUser={() => setInviteUserModalOpen(true)} + onOpenBots={() => setBotsModalOpen(true)} /> {topSlot && ( @@ -2024,8 +2044,14 @@ export const ChatArea: React.FC<{ Embedded iframes (YouTube) are suspended by the browser, but we explicitly stop embed media on channel switch (see effect below). */}
+ {aliveChannels.map( ({ key, @@ -2313,11 +2339,84 @@ export const ChatArea: React.FC<{ inputElement={inputRef.current} /> - {/* obsidianirc/cmdslist: slash-command suggestion popover */} + {/* obsidianirc/cmdslist + draft/bot-cmds: slash-command suggestion popover */} {(() => { const srv = servers.find((s) => s.id === selectedServerId); - const cmds = srv?.cmdsAvailable ?? []; - if (cmds.length === 0) return null; + const suggestions: SlashSuggestion[] = []; + const seen = new Set(); + + // 1. Client-side handlers (e.g. /me, /msg, /nick) — + // listed first because they own those names and the + // server's cmdslist may shadow them. + const inChannelView = !!selectedChannel; + for (const c of getClientCommands()) { + if (c.scope === "channel-only" && !inChannelView) continue; + suggestions.push({ + name: c.name, + description: c.description, + options: c.options, + source: { kind: "client" }, + }); + seen.add(c.name.toLowerCase()); + } + + // 2. PushBot schemas — done before the server's + // cmdsAvailable so that bot-defined names win the + // dedup set. Channel-scope bots only appear when + // they're a member of the active channel; server- + // scope bots (helpbot, dicebot) are reachable from + // any context and always show. + if (srv?.botCommands) { + const chanUsers = new Set( + selectedChannel?.users.map((u) => + u.username.toLowerCase(), + ) ?? [], + ); + for (const [botNick, list] of Object.entries( + srv.botCommands, + )) { + const botScope: "channel" | "server" = + srv.bots?.[botNick]?.scope ?? "channel"; + const inChannel = chanUsers.has(botNick.toLowerCase()); + if ( + botScope === "channel" && + (!selectedChannel || !inChannel) + ) + continue; + const scope: "channel" | "server" = botScope; + for (const c of list) { + // Cross-bot collisions intentionally NOT dedup'd: + // both /help entries (helpbot + another) should + // show so the user picks which bot to invoke. + // The picker fills `/cmd@botnick` so dispatch + // routes unambiguously. + suggestions.push({ + name: c.name, + description: c.description, + options: c.options, + source: { kind: "bot", botNick, scope }, + }); + // Reserve the bare name so the server's + // cmdsAvailable can't list a duplicate /HELP + // after a bot's /help. + seen.add(c.name.toLowerCase()); + } + } + } + + // 3. Server-provided permission list (obsidianirc/cmdslist). + // No schema -- just names; skip anything the client + // or a bot already owns so /help, /report etc. don't + // get shadowed by the built-in /HELP. + for (const name of srv?.cmdsAvailable ?? []) { + if (seen.has(name.toLowerCase())) continue; + suggestions.push({ + name, + source: { kind: "server" }, + }); + seen.add(name.toLowerCase()); + } + if (suggestions.length === 0) return null; const slashActive = getActiveSlashQuery( slashInputValue, @@ -2327,12 +2426,36 @@ export const ChatArea: React.FC<{ { - // Replace the partial command with / + space - // and put the cursor right after the space. - const next = `/${cmd} `; + onSelect={(sug) => { + // Bot commands with declared options open the + // param modal so values are entered via typed + // form controls (user/channel pickers, date, + // etc). Bots without options and non-bot + // sources stay on the freeform inline path. + if ( + sug.source.kind === "bot" && + (sug.options?.length ?? 0) > 0 + ) { + const botNick = sug.source.botNick; + setParamModal({ + botNick, + command: { + name: sug.name, + description: sug.description, + options: sug.options, + }, + }); + setSlashInputValue(""); + applyText(""); + return; + } + const target = + sug.source.kind === "bot" + ? `@${sug.source.botNick}` + : ""; + const next = `/${sug.name}${target} `; applyText(next); cursorPositionRef.current = next.length; setSlashInputValue(""); @@ -2349,6 +2472,83 @@ export const ChatArea: React.FC<{ ); })()} + {/* Per-arg hint shown after the cmd name + a space. + Pulls schemas from both client-side commands and + any cached PushBot schemas. */} + {(() => { + const srv = servers.find((s) => s.id === selectedServerId); + const schemas: Record< + string, + { + command: import("../../types").BotCommand; + source: "client" | "bot"; + botNick?: string; + scope: "channel" | "server" | "dm"; + } + > = {}; + + // Client-side commands first; bot schemas overwrite + // only if there's a real collision (none in practice + // because the client owns /me, /msg, etc). + for (const c of getClientCommands()) { + schemas[c.name.toLowerCase()] = { + command: { + name: c.name, + description: c.description, + options: c.options, + }, + source: "client", + scope: "dm", + }; + } + + if (srv?.botCommands) { + const chanUsers = new Set( + selectedChannel?.users.map((u) => + u.username.toLowerCase(), + ) ?? [], + ); + for (const [botNick, list] of Object.entries( + srv.botCommands, + )) { + const botScope: "channel" | "server" = + srv.bots?.[botNick]?.scope ?? "channel"; + const inChannel = chanUsers.has(botNick.toLowerCase()); + if ( + botScope === "channel" && + (!selectedChannel || !inChannel) + ) + continue; + for (const c of list) { + const entry = { + command: c, + source: "bot" as const, + botNick, + scope: botScope, + }; + // Specific key handles `/cmd@bot foo` lookups; + // bare key is a fallback for `/cmd foo` (last + // bot wins, which matches dispatch's fallback + // ordering). + schemas[ + `${c.name.toLowerCase()}@${botNick.toLowerCase()}` + ] = entry; + schemas[c.name.toLowerCase()] = entry; + } + } + } + + if (Object.keys(schemas).length === 0) return null; + return ( + + ); + })()} + {/* Members dropdown triggered by @ button */} )} + {selectedServerId && ( + setBotsModalOpen(false)} + serverId={selectedServerId} + onPickCommand={(botNick, command) => { + setParamModal({ botNick, command }); + }} + /> + )} {selectedServerId && ( , document.body, )} + {paramModal && + selectedServerId && + createPortal( + u.username) ?? []} + joinedChannels={ + servers + .find((s) => s.id === selectedServerId) + ?.channels.map((c) => c.name) ?? [] + } + onClose={() => setParamModal(null)} + />, + document.body, + )}
); }; diff --git a/src/components/layout/ChatHeader.tsx b/src/components/layout/ChatHeader.tsx index 63701e6a..be65a691 100644 --- a/src/components/layout/ChatHeader.tsx +++ b/src/components/layout/ChatHeader.tsx @@ -16,6 +16,7 @@ import { FaList, FaMicrophone, FaPenAlt, + FaRobot, FaSearch, FaThumbtack, FaTimes, @@ -31,6 +32,7 @@ import { canShowAvatarUrl, mediaLevelToSettings } from "../../lib/mediaUtils"; import { isTauriMobile } from "../../lib/platformUtils"; import useStore, { loadSavedMetadata } from "../../store"; import type { Channel, PrivateChat, User } from "../../types"; +import { BotToolsHistoryButton } from "../ui/BotToolsHistoryButton"; import HeaderOverflowMenu, { type HeaderOverflowMenuItem, } from "../ui/HeaderOverflowMenu"; @@ -56,6 +58,7 @@ interface ChatHeaderProps { onToggleNotificationVolume: () => void; onOpenChannelSettings: () => void; onOpenInviteUser: () => void; + onOpenBots: () => void; } export const ChatHeader: React.FC = ({ @@ -75,6 +78,7 @@ export const ChatHeader: React.FC = ({ onToggleNotificationVolume, onOpenChannelSettings, onOpenInviteUser, + onOpenBots, }) => { const { t } = useLingui(); const { @@ -320,6 +324,12 @@ export const ChatHeader: React.FC = ({ onClick: onOpenInviteUser, show: !!selectedChannel, }, + { + label: t`Bots`, + icon: , + onClick: onOpenBots, + show: !!selectedServerId, + }, { label: "Play Tic-Tac-Toe", icon: , @@ -586,6 +596,14 @@ export const ChatHeader: React.FC = ({ > + )} + {/* AI workflow history — renders nothing when empty */} + {selectedServerId && ( + + )} {/* Search */} + {selectedServerId && ( + + )} + ); +}; + +export default BotToolsMessagePill; diff --git a/src/components/message/BotToolsPlaceholderBody.tsx b/src/components/message/BotToolsPlaceholderBody.tsx new file mode 100644 index 00000000..09724e5a --- /dev/null +++ b/src/components/message/BotToolsPlaceholderBody.tsx @@ -0,0 +1,83 @@ +import { Trans } from "@lingui/react/macro"; +import type React from "react"; +import { + FaCheck, + FaExclamationTriangle, + FaSpinner, + FaTimes, + FaTimesCircle, +} from "react-icons/fa"; +import type { AiStep } from "../../store"; +import useStore from "../../store"; + +interface BotToolsPlaceholderBodyProps { + serverId: string; + workflowId: string; +} + +function stepGlyph(state: AiStep["state"]) { + switch (state) { + case "complete": + return ; + case "failed": + return ; + case "cancelled": + return ; + case "pending-approval": + return ; + default: + return ( + + ); + } +} + +function stepLabel(step: AiStep): string { + if (step.label) return step.label; + if (step.type === "tool-call" || step.type === "tool-result") + return step.tool ?? step.type; + if (step.type === "reasoning") return "Reasoning"; + return "Text"; +} + +export const BotToolsPlaceholderBody: React.FC< + BotToolsPlaceholderBodyProps +> = ({ serverId, workflowId }) => { + const workflow = useStore((s) => s.aiWorkflows[serverId]?.[workflowId]); + + if (!workflow) { + return ( +
+ + Starting workflow… +
+ ); + } + + const lastStep = workflow.steps[workflow.steps.length - 1]; + + return ( +
+
+ + {workflow.name ? ( + + {workflow.name} + + ) : ( + Working… + )} +
+ {lastStep && ( +
+ {stepGlyph(lastStep.state)} + + {stepLabel(lastStep)} + +
+ )} +
+ ); +}; + +export default BotToolsPlaceholderBody; diff --git a/src/components/message/MessageItem.tsx b/src/components/message/MessageItem.tsx index 24b1049b..fe6e123d 100644 --- a/src/components/message/MessageItem.tsx +++ b/src/components/message/MessageItem.tsx @@ -23,6 +23,9 @@ import useStore, { loadSavedMetadata } from "../../store"; import type { MessageType, PrivateChat, User } from "../../types"; import MessageBottomSheet from "../mobile/MessageBottomSheet"; import { EnhancedLinkWrapper } from "../ui/LinkWrapper"; +import { BotInvocationChip } from "./BotInvocationChip"; +import { BotToolsMessagePill } from "./BotToolsMessagePill"; +import { BotToolsPlaceholderBody } from "./BotToolsPlaceholderBody"; import type { CollapsibleMessageHandle } from "./CollapsibleMessage"; import { InviteMessage } from "./InviteMessage"; import { @@ -723,6 +726,9 @@ export const MessageItem = memo((props: MessageItemProps) => { )}
+ {message.replyMessage && ( { onOpenProfile={onOpenProfile} /> ) : ( - // Unknown type (needs probe) or multi-URL: show text body + // The workflow pill is absolutely positioned in the + // avatar gutter to the LEFT of the body, so the body + // stays at its natural alignment and isn't pushed + // sideways by the pill's width.
- {collapsibleContent} + + + + {message.botToolsPending && message.botToolsWorkflowId ? ( + + ) : ( + collapsibleContent + )}
)} diff --git a/src/components/ui/AddServerModal.tsx b/src/components/ui/AddServerModal.tsx index 25944797..7a3ec8da 100644 --- a/src/components/ui/AddServerModal.tsx +++ b/src/components/ui/AddServerModal.tsx @@ -114,9 +114,11 @@ export const AddServerModal: React.FC = () => { let finalHost = serverHost; if (isTauri()) { const port = Number.parseInt(serverPort, 10); + // Strip scheme AND any embedded :port / path so we don't end up + // appending port twice (e.g. ircs://host:6697:6697). const cleanHost = serverHost - .replace(/^(https?|wss?|ircs?):\/\//, "") - .replace(/:\d+$/, ""); + .replace(/^(https?|wss?|ircs?|irc):\/\//, "") + .replace(/[:/].*$/, ""); finalHost = useWebSocket ? `wss://${cleanHost}:${port}` : `ircs://${cleanHost}:${port}`; diff --git a/src/components/ui/BotToolsCard.tsx b/src/components/ui/BotToolsCard.tsx new file mode 100644 index 00000000..61153250 --- /dev/null +++ b/src/components/ui/BotToolsCard.tsx @@ -0,0 +1,667 @@ +import { Trans, useLingui } from "@lingui/react/macro"; +import type React from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { + FaArrowRight, + FaCheck, + FaChevronDown, + FaChevronUp, + FaExclamationTriangle, + FaProjectDiagram, + FaSpinner, + FaTimes, + FaTimesCircle, +} from "react-icons/fa"; +import type { AiStep, AiWorkflow } from "../../store"; +import useStore from "../../store"; + +// Scroll the chat-side message with the given internal id into view and +// run the same .message-flash highlight that reply-jump uses, so the +// deep link from the workflow card lands somewhere attention-grabbing. +function scrollToMessageId(internalId: string): boolean { + const el = document.querySelector(`[data-message-id="${internalId}"]`); + if (!el) return false; + el.scrollIntoView({ behavior: "smooth", block: "center" }); + el.classList.add("message-flash"); + setTimeout(() => el.classList.remove("message-flash"), 2000); + return true; +} + +interface BotToolsCardProps { + workflow: AiWorkflow; +} + +const STEP_TYPE_ACCENT: Record = { + reasoning: "bg-purple-400", + "tool-call": "bg-cyan-400", + "tool-result": "bg-emerald-400", + text: "bg-discord-text-muted", +}; + +function workflowHeaderIcon(state: AiWorkflow["state"]) { + switch (state) { + case "complete": + return ; + case "failed": + return ; + case "cancelled": + return ; + default: + return ; + } +} + +// Render an arbitrary JSON value as a nested set of badges. Primitives +// become inline coloured chips; arrays and objects nest under a tinted +// left rule with their entries laid out one-per-row. Designed for the +// tool-call args dump in particular, where flat `JSON.stringify` is +// hard to scan once nesting deepens. +const JsonBadges: React.FC<{ value: unknown }> = ({ value }) => { + if (value === null) + return ( + + null + + ); + if (typeof value === "string") + return ( + + "{value}" + + ); + if (typeof value === "number") + return {value}; + if (typeof value === "boolean") + return ( + {String(value)} + ); + if (Array.isArray(value)) { + if (value.length === 0) + return ( + [ ] + ); + return ( +
+ {value.map((v, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: index is the only stable id for a positional array entry here +
+ + [{i}] + +
+ +
+
+ ))} +
+ ); + } + if (typeof value === "object") { + const entries = Object.entries(value as Record); + if (entries.length === 0) + return ( + + {"{ }"} + + ); + return ( +
+ {entries.map(([k, v]) => { + const isContainer = + v !== null && typeof v === "object" && Object.keys(v).length > 0; + return ( +
+ + {k} + +
+ +
+
+ ); + })} +
+ ); + } + return {String(value)}; +}; + +// Render an AiStep's content payload (string or JSON) in the same +// monospace box style used everywhere else in the card. +const ContentBox: React.FC<{ content: unknown }> = ({ content }) => { + if (content === undefined || content === null) return null; + if (typeof content === "string") { + return ( +
+        {content}
+      
+ ); + } + // Tool-call args (and any other structured payload) — render as + // recursive badges. A flat
 JSON dump is hard to scan once
+  // arguments grow nested; a key→value chip tree mirrors how the
+  // model actually thought about the call.
+  return (
+    
+ +
+ ); +}; + +function stepStateGlyph(state: AiStep["state"]) { + switch (state) { + case "complete": + return ; + case "failed": + return ; + case "cancelled": + return ; + case "pending-approval": + return ; + default: + return ( + + ); + } +} + +const StepRow: React.FC<{ + accent: string; + isFirst: boolean; + isLast: boolean; + children: React.ReactNode; +}> = ({ accent, isFirst, isLast, children }) => ( +
+
+ {/* Vertical connector line: spans the full row height so + consecutive rows visually join; clipped on the first / + last row so it doesn't hang past the outermost dots. */} + + +
+
{children}
+
+); + +// Render a single reasoning / text step. Colored dot, terse header, +// then the content payload in a mono box if present. +const Step: React.FC<{ + step: AiStep; + isFirst: boolean; + isLast: boolean; +}> = ({ step, isFirst, isLast }) => { + const accent = STEP_TYPE_ACCENT[step.type] ?? STEP_TYPE_ACCENT.text; + + const headerKind = + step.type === "reasoning" ? Reasoning : Text; + + return ( + +
+ + {headerKind} + + {step.label && ( + + {step.label} + + )} + {stepStateGlyph(step.state)} +
+ {step.content !== undefined && step.content !== null && ( +
+ +
+ )} + {step.truncated && ( +
+ output truncated +
+ )} +
+ ); +}; + +// Render a tool-call together with its matching tool-result as a +// single row -- one header (the tool name), then IN (call args) and +// OUT (result) stacked below. Either side may be absent during the +// in-between window after the call lands and before the result +// arrives. +const ToolPairStep: React.FC<{ + call?: AiStep; + result?: AiStep; + isFirst: boolean; + isLast: boolean; +}> = ({ call, result, isFirst, isLast }) => { + const accent = STEP_TYPE_ACCENT["tool-call"]; + const primary = result ?? call; + const tool = primary?.tool ?? "tool"; + const label = primary?.label; + const state = primary?.state ?? "running"; + const truncated = call?.truncated || result?.truncated; + + return ( + +
+ + Tool + + {tool} + {label && ( + + {label} + + )} + {stepStateGlyph(state)} +
+ {call?.content !== undefined && call?.content !== null && ( +
+
+ IN +
+ +
+ )} + {result?.content !== undefined && result?.content !== null && ( +
+
+ OUT +
+ +
+ )} + {truncated && ( +
+ output truncated +
+ )} +
+ ); +}; + +type RenderItem = + | { kind: "single"; key: string; step: AiStep } + | { + kind: "tool-pair"; + key: string; + call?: AiStep; + result?: AiStep; + approvalSid?: string; + }; + +function buildRenderItems(steps: AiStep[]): RenderItem[] { + const items: RenderItem[] = []; + const paired = new Set(); + for (let i = 0; i < steps.length; i++) { + if (paired.has(i)) continue; + const s = steps[i]; + if (s.type === "tool-call") { + let result: AiStep | undefined; + for (let j = i + 1; j < steps.length; j++) { + if (paired.has(j)) continue; + const t = steps[j]; + if (t.type === "tool-result" && t.tool === s.tool) { + result = t; + paired.add(j); + break; + } + } + items.push({ + kind: "tool-pair", + key: s.sid, + call: s, + result, + approvalSid: + s.state === "pending-approval" + ? s.sid + : result?.state === "pending-approval" + ? result.sid + : undefined, + }); + } else if (s.type === "tool-result") { + // tool-result with no preceding tool-call: render standalone + items.push({ + kind: "tool-pair", + key: s.sid, + result: s, + approvalSid: s.state === "pending-approval" ? s.sid : undefined, + }); + } else { + items.push({ kind: "single", key: s.sid, step: s }); + } + } + return items; +} + +export const BotToolsCard: React.FC = ({ workflow }) => { + const { t } = useLingui(); + const setCollapsed = useStore((s) => s.aiWorkflowSetCollapsed); + const dismiss = useStore((s) => s.aiWorkflowDismiss); + const sendAction = useStore((s) => s.aiSendAction); + // For "Responded in chat" — map the workflow's finalMsgid (an IRC + // msgid string) to the internal Message.id we use as the DOM key. + // Only takes a single string out of the store so we don't re-render + // the card on every unrelated message arrival. + const finalMessageInternalId = useStore((s) => { + if (!workflow.finalMsgid) return undefined; + for (const bucket of Object.values(s.messages)) { + for (const m of bucket) { + if ( + m.serverId === workflow.serverId && + m.msgid === workflow.finalMsgid + ) { + return m.id; + } + } + } + return undefined; + }); + + const isTerminal = + workflow.state === "complete" || + workflow.state === "failed" || + workflow.state === "cancelled"; + + const onToggle = () => + setCollapsed(workflow.serverId, workflow.id, !workflow.collapsed); + + const onCancel = (e: React.MouseEvent) => { + e.stopPropagation(); + sendAction(workflow.serverId, workflow.senderNick, { + msg: "action", + action: "cancel", + target: workflow.id, + }); + }; + + const onApprove = (sid: string) => { + sendAction(workflow.serverId, workflow.senderNick, { + msg: "action", + action: "approve", + target: sid, + }); + }; + + const onReject = (sid: string) => { + sendAction(workflow.serverId, workflow.senderNick, { + msg: "action", + action: "reject", + target: sid, + }); + }; + + const pendingApprovals = workflow.steps.filter( + (s) => s.state === "pending-approval", + ); + + // Auto-dismiss countdown — once a workflow is terminal AND collapsed, + // start a 5s countdown. The user can re-expand or hover to pause. + // Expanding the card resets the timer entirely (they're reviewing the + // run); collapsing again restarts it. + const FADE_SECONDS = 5; + const [secondsLeft, setSecondsLeft] = useState(null); + const [paused, setPaused] = useState(false); + useEffect(() => { + if (!isTerminal || !workflow.collapsed) { + setSecondsLeft(null); + return; + } + setSecondsLeft(FADE_SECONDS); + }, [isTerminal, workflow.collapsed]); + // biome-ignore lint/correctness/useExhaustiveDependencies: dismiss is a Zustand action with unstable ref + useEffect(() => { + if (secondsLeft === null || paused) return; + if (secondsLeft <= 0) { + dismiss(workflow.serverId, workflow.id); + return; + } + const t = setTimeout(() => setSecondsLeft((s) => (s ?? 0) - 1), 1000); + return () => clearTimeout(t); + }, [secondsLeft, paused, workflow.serverId, workflow.id]); + + // Auto-scroll the expanded step list. We can't check "is the user + // at the bottom?" inside the content-update effect, because by the + // time the effect runs the new content has already grown + // scrollHeight and the old scrollTop no longer qualifies as + // bottom. Instead, track stickiness via the scroll event: whenever + // the user scrolls (or we programmatically scroll), recompute + // whether they're at the bottom. On content update, honour that + // flag. Reset to sticky on each fresh expansion so a freshly- + // opened card always lands on the latest content. + const bodyRef = useRef(null); + const isStickyToBottom = useRef(true); + const onBodyScroll = useCallback(() => { + const el = bodyRef.current; + if (!el) return; + const SCROLL_TOLERANCE = 24; + isStickyToBottom.current = + el.scrollHeight - (el.scrollTop + el.clientHeight) <= SCROLL_TOLERANCE; + }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: track step count + updatedAt because step content updates don't change the array reference + useEffect(() => { + const el = bodyRef.current; + if (!el || workflow.collapsed) { + isStickyToBottom.current = true; + return; + } + if (isStickyToBottom.current) { + el.scrollTop = el.scrollHeight; + } + }, [workflow.collapsed, workflow.steps.length, workflow.updatedAt]); + + // Linearly fade the card from full opacity to a barely-visible + // ghost over the entire countdown. Bottoms out at 0.15 so a + // re-hover (which pauses the timer) doesn't land on an invisible + // target. + const fadeOpacity = + secondsLeft !== null ? Math.max(0.15, secondsLeft / FADE_SECONDS) : 1; + + return ( +
setPaused(true)} + onMouseLeave={() => setPaused(false)} + className="w-[680px] max-w-full bg-discord-dark-300/95 backdrop-blur-sm border border-discord-dark-400 rounded-lg shadow-xl overflow-hidden" + > + {/* Header — collapsed row.
with role=button because the + row holds Dismiss/Cancel + )} + {!isTerminal && ( + + )} + + {workflow.collapsed ? ( + + ) : ( + + )} + +
+ + {/* Countdown drain on a collapsed, terminal card */} + {secondsLeft !== null && ( +
+
+
+ )} + + {/* Expanded body — step list. No horizontal divider between + steps -- a vertical connector through the dot column does + the same job and reads more like a pipeline. */} + {!workflow.collapsed && ( +
+ {workflow.steps.length === 0 ? ( +
+ Waiting for first step… +
+ ) : ( + (() => { + const items = buildRenderItems(workflow.steps); + return items.map((item, i) => { + const isFirst = i === 0; + const isLast = i === items.length - 1; + return ( +
+ {item.kind === "single" ? ( + + ) : ( + + )} + {item.kind === "tool-pair" && item.approvalSid && ( +
+ + +
+ )} + {item.kind === "single" && + item.step.state === "pending-approval" && ( +
+ + +
+ )} +
+ ); + }); + })() + )} +
+ )} + + {/* Collapsed-state attention hint when an approval is pending */} + {workflow.collapsed && pendingApprovals.length > 0 && ( +
+ + {pendingApprovals.length} step(s) awaiting approval +
+ )} + + {/* Deep link to the bot's final PRIVMSG once it's landed in chat */} + {isTerminal && workflow.finalMsgid && ( + + )} +
+ ); +}; + +export default BotToolsCard; diff --git a/src/components/ui/BotToolsHistoryButton.tsx b/src/components/ui/BotToolsHistoryButton.tsx new file mode 100644 index 00000000..d59c3d83 --- /dev/null +++ b/src/components/ui/BotToolsHistoryButton.tsx @@ -0,0 +1,152 @@ +import { Trans, useLingui } from "@lingui/react/macro"; +import type React from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { + FaCheck, + FaProjectDiagram, + FaSpinner, + FaTimesCircle, +} from "react-icons/fa"; +import { countableSteps } from "../../lib/botTools"; +import type { AiWorkflow } from "../../store"; +import useStore from "../../store"; + +interface BotToolsHistoryButtonProps { + serverId: string; + channel: string | null; +} + +function stateGlyph(state: AiWorkflow["state"]) { + switch (state) { + case "complete": + return ; + case "failed": + case "cancelled": + return ; + default: + return ( + + ); + } +} + +function fmtAgo(ts: number): string { + const s = Math.max(1, Math.round((Date.now() - ts) / 1000)); + if (s < 60) return `${s}s`; + const m = Math.round(s / 60); + if (m < 60) return `${m}m`; + const h = Math.round(m / 60); + if (h < 24) return `${h}h`; + return `${Math.round(h / 24)}d`; +} + +// Workflow history button + popover for the chat header. Renders nothing +// when no workflows exist for the current target -- so the icon doesn't +// take up space until there's something to surface. +export const BotToolsHistoryButton: React.FC = ({ + serverId, + channel, +}) => { + const { t } = useLingui(); + const reopen = useStore((s) => s.aiWorkflowReopen); + const serverWorkflows = useStore((s) => s.aiWorkflows[serverId]); + + // All workflows for this server filtered to the current target. + // Newest-first so the most-relevant ones are at the top of the list. + const workflows = useMemo(() => { + if (!serverWorkflows || !channel) return []; + return Object.values(serverWorkflows) + .filter((w) => w.channel === channel) + .sort((a, b) => b.startedAt - a.startedAt); + }, [serverWorkflows, channel]); + + const [open, setOpen] = useState(false); + const rootRef = useRef(null); + + // Close popover on outside click or Escape. + useEffect(() => { + if (!open) return; + const onClick = (e: MouseEvent) => { + if (!rootRef.current?.contains(e.target as Node)) setOpen(false); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setOpen(false); + }; + document.addEventListener("mousedown", onClick); + document.addEventListener("keydown", onKey); + return () => { + document.removeEventListener("mousedown", onClick); + document.removeEventListener("keydown", onKey); + }; + }, [open]); + + if (workflows.length === 0) return null; + + return ( +
+ + {open && ( +
+
+ + Workflow history + + {workflows.length} +
+
    + {workflows.map((w) => ( +
  • + +
  • + ))} +
+
+ )} +
+ ); +}; + +export default BotToolsHistoryButton; diff --git a/src/components/ui/BotToolsTray.tsx b/src/components/ui/BotToolsTray.tsx new file mode 100644 index 00000000..8401df65 --- /dev/null +++ b/src/components/ui/BotToolsTray.tsx @@ -0,0 +1,47 @@ +import type React from "react"; +import { useMemo } from "react"; +import useStore from "../../store"; +import { BotToolsCard } from "./BotToolsCard"; + +interface BotToolsTrayProps { + serverId: string | null; + // Channel name or PM target the user is currently viewing. Workflows + // are scoped to their announce-channel so we only show what's relevant + // to the user's current focus. + channel: string | null; +} + +export const BotToolsTray: React.FC = ({ + serverId, + channel, +}) => { + const serverWorkflows = useStore((s) => + serverId ? s.aiWorkflows[serverId] : undefined, + ); + + const visible = useMemo(() => { + if (!serverWorkflows || !channel) return []; + return ( + Object.values(serverWorkflows) + // Skip historical workflows -- those replay through CHATHISTORY + // when joining a channel and shouldn't pop a wall of cards. + // They still appear in the history popover for inspection. + .filter((w) => !w.dismissed && !w.historical && w.channel === channel) + .sort((a, b) => b.startedAt - a.startedAt) + ); + }, [serverWorkflows, channel]); + + if (visible.length === 0) return null; + + return ( +
+ {visible.map((w) => ( +
+ +
+ ))} +
+ ); +}; + +export default BotToolsTray; diff --git a/src/components/ui/BotsModal.tsx b/src/components/ui/BotsModal.tsx new file mode 100644 index 00000000..69601c43 --- /dev/null +++ b/src/components/ui/BotsModal.tsx @@ -0,0 +1,592 @@ +// Bot management modal — directory view of the obby.world/channel-bots +// cap state. Mirrors UserSettings' layout: desktop = backdrop + centered +// card with a left sidebar (filterable bot list) and right content pane +// (selected bot's detail); mobile = full-screen portal with a two-view +// drill-in (list → detail, back button to return). +// +// Bot lifecycle pushes from the IRCd land in server.bots; this modal is +// a pure read of that map plus a few send-and-forget /PUSHBOT subcommand +// buttons for IRCops. + +import { t } from "@lingui/core/macro"; +import { Trans } from "@lingui/react/macro"; +import type React from "react"; +import { useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import { FaChevronLeft, FaCircle, FaRobot, FaTimes } from "react-icons/fa"; +import { useMediaQuery } from "../../hooks/useMediaQuery"; +import { useModalBehavior } from "../../hooks/useModalBehavior"; +import ircClient from "../../lib/ircClient"; +import useStore from "../../store"; +import type { BotCommand, PushBotInfo } from "../../types"; + +interface BotsModalProps { + isOpen: boolean; + onClose: () => void; + serverId: string; + /** Invoked when the user clicks one of the bot's slash commands. + * ChatArea opens the slash-command param modal in response. */ + onPickCommand?: (botNick: string, command: BotCommand) => void; +} + +type FilterMode = "all" | "server" | "channel"; + +const STATUS_BADGE: Record< + PushBotInfo["status"], + { label: string; cls: string } +> = { + active: { + label: "active", + cls: "bg-emerald-700/40 text-emerald-300 border border-emerald-600/50", + }, + pending: { + label: "pending", + cls: "bg-amber-700/40 text-amber-300 border border-amber-600/50", + }, + suspended: { + label: "suspended", + cls: "bg-red-700/40 text-red-300 border border-red-600/50", + }, + deleted: { + label: "deleted", + cls: "bg-discord-dark-400 text-discord-text-muted border border-discord-dark-500", + }, +}; + +const SCOPE_BADGE: Record< + PushBotInfo["scope"], + { label: string; title: string; cls: string } +> = { + server: { + label: "server", + title: "Server-wide bot — reachable from any channel", + cls: "bg-discord-primary/30 text-discord-primary border border-discord-primary/40", + }, + channel: { + label: "channel", + title: "Channel bot — only in joined channels", + cls: "bg-amber-700/30 text-amber-300 border border-amber-600/40", + }, +}; + +const FILTER_LABELS: Record = { + all: "All", + server: "Server-wide", + channel: "Channel", +}; + +// ── child components ────────────────────────────────────────────────── + +interface BotRowProps { + bot: PushBotInfo; + selected: boolean; + onSelect: () => void; +} + +const BotRow: React.FC = ({ bot, selected, onSelect }) => ( + +); + +interface BotDetailProps { + bot: PushBotInfo; + isOper: boolean; + onAction: (subcmd: string) => void; + onPickCommand?: (command: BotCommand) => void; +} + +const BotDetail: React.FC = ({ + bot, + isOper, + onAction, + onPickCommand, +}) => ( +
+
+

{bot.nick}

+ + {SCOPE_BADGE[bot.scope].label} + + + {STATUS_BADGE[bot.status].label} + + + + {bot.online ? t`gateway online` : t`offline`} + +
+ {bot.realname && ( +
{bot.realname}
+ )} + +
+
+ Transport +
+
{bot.transport}
+
+ Source +
+
+ {bot.from_config ? t`config-defined` : t`self-registered`} +
+ {bot.scope === "channel" && ( + <> +
+ Channels +
+
+ {bot.channels.length ? bot.channels.join(", ") : "—"} +
+ + )} + {bot.webhook_url !== undefined && ( + <> +
+ Webhook +
+
+ {bot.webhook_url || "—"} + {bot.webhook_suspended && ( + + (suspended) + + )} +
+ + )} +
+ +
+

+ Slash commands +

+ {bot.commands.length === 0 ? ( +
+ Bot hasn't registered any slash commands yet. +
+ ) : ( +
    + {bot.commands.map((cmd) => { + const body = ( + <> +
    + /{cmd.name} + {(cmd.options ?? []).map((o) => ( + + {o.required ? `<${o.name}>` : `[${o.name}]`} + + ))} +
    + {cmd.description && ( +
    + {cmd.description} +
    + )} + + ); + return onPickCommand ? ( +
  • + +
  • + ) : ( +
  • + {body} +
  • + ); + })} +
+ )} +
+ + {isOper && !bot.from_config && ( +
+

+ Operator actions +

+
+ {bot.status === "pending" && ( + + )} + {bot.status === "active" && ( + + )} + {bot.status === "suspended" && ( + + )} + +
+
+ )} + {isOper && bot.from_config && ( +
+ + Config-defined bot. Edit obbyircd.conf and /REHASH to change state. + +
+ )} +
+); + +// ── main modal ──────────────────────────────────────────────────────── + +const BotsModal: React.FC = ({ + isOpen, + onClose, + serverId, + onPickCommand, +}) => { + const isMobile = useMediaQuery("(max-width: 768px)"); + const server = useStore((s) => s.servers.find((srv) => srv.id === serverId)); + const currentUser = useStore((s) => s.currentUser); + const [filter, setFilter] = useState("all"); + const [query, setQuery] = useState(""); + const [selectedNick, setSelectedNick] = useState(null); + const [mobileView, setMobileView] = useState<"list" | "detail">("list"); + + const { getBackdropProps, getContentProps } = useModalBehavior({ + onClose, + isOpen, + }); + + const isOper = (() => { + const myNick = currentUser?.username; + if (!myNick || !server) return false; + const me = server.users.find((u) => u.username === myNick); + return !!me?.isIrcOp; + })(); + + const bots: PushBotInfo[] = useMemo(() => { + if (!server?.bots) return []; + // Reachability filter: server-scope bots are always shown + // (reachable from any context). Channel-scope bots only appear + // when the local user still shares a channel with them -- this + // matches the server-side discovery contract (server-scope on + // burst, channel-scope on LOCAL_JOIN of a shared channel) and + // hides stale entries left over after the user parts a channel. + const joinedChannels = new Set( + (server.channels ?? []).map((c) => c.name.toLowerCase()), + ); + const reachable = (b: PushBotInfo) => { + // Opers manage bots they may not share a channel with; don't + // hide channel-scope entries from them. + if (isOper) return true; + if (b.scope === "server") return true; + if (!b.channels?.length) return false; + return b.channels.some((ch) => joinedChannels.has(ch.toLowerCase())); + }; + const all = Object.values(server.bots); + return all + .filter(reachable) + .filter((b) => filter === "all" || b.scope === filter) + .filter((b) => + query + ? b.nick.toLowerCase().includes(query.toLowerCase()) || + (b.realname ?? "").toLowerCase().includes(query.toLowerCase()) + : true, + ) + .sort((a, b) => { + const sa = a.status === "active" ? 0 : a.status === "pending" ? 1 : 2; + const sb = b.status === "active" ? 0 : b.status === "pending" ? 1 : 2; + if (sa !== sb) return sa - sb; + return a.nick.localeCompare(b.nick); + }); + }, [server?.bots, server?.channels, filter, query, isOper]); + + const selected = selectedNick + ? server?.bots?.[selectedNick.toLowerCase()] + : undefined; + + const send = (subcmd: string) => { + if (!isOper || !selected) return; + ircClient.sendRaw(serverId, `PUSHBOT ${subcmd} ${selected.nick}`); + if (subcmd === "DELETE") { + setSelectedNick(null); + if (isMobile) setMobileView("list"); + } + }; + + const onPickBot = (nick: string) => { + setSelectedNick(nick); + if (isMobile) setMobileView("detail"); + }; + + if (!isOpen) return null; + + // ── list-pane content, shared between mobile and desktop ──────────── + const listPane = ( + <> +
+ setQuery(e.target.value)} + placeholder={t`Search bots`} + className="w-full bg-discord-dark-400 rounded px-2 py-1.5 text-sm text-discord-text-normal placeholder:text-discord-text-muted border border-discord-dark-300 focus:outline-none focus:border-discord-primary" + /> +
+ {(["all", "server", "channel"] as FilterMode[]).map((f) => ( + + ))} +
+ {bots.length} +
+
+
+
+ {bots.length === 0 ? ( +
+ No bots registered on this network yet. +
+ ) : ( + bots.map((b) => ( + onPickBot(b.nick)} + /> + )) + )} +
+ + ); + + const detailPane = selected ? ( + { + onPickCommand(selected.nick, cmd); + onClose(); + } + : undefined + } + /> + ) : ( +
+ + + Select a bot on the left to see its commands and management actions. + +
+ ); + + // ── mobile: full-screen portal with two views ────────────────────── + if (isMobile) { + const portalTarget = document.getElementById("root") || document.body; + return createPortal( +
+ {mobileView === "list" ? ( + <> +
+

+ + Bots +

+ +
+ {listPane} + + ) : ( + <> +
+
+ +

+ {selected?.nick ?? t`Bot`} +

+
+ +
+
{detailPane}
+ + )} +
, + portalTarget, + ); + } + + // ── desktop: backdrop + centered card with sidebar + content ─────── + return ( +
+
+ {/* Sidebar — list of bots */} +
+
+ +

+ Bots +

+
+ {listPane} +
+ + {/* Main content — bot detail */} +
+
+

+ {selected ? selected.nick : t`Bots on this network`} +

+ +
+
+
{detailPane}
+
+
+
+
+ ); +}; + +export default BotsModal; diff --git a/src/components/ui/SlashCommandParamModal.tsx b/src/components/ui/SlashCommandParamModal.tsx new file mode 100644 index 00000000..e2d958e2 --- /dev/null +++ b/src/components/ui/SlashCommandParamModal.tsx @@ -0,0 +1,438 @@ +// Param-collection modal opened when a user selects a slash command +// from the SlashCommandPopover and that command declares any +// `options[]`. Renders one form field per option, typed by +// `option.type`: +// +// string text input +// int / number numeric input (step=1 for int, any for number) +// bool checkbox +// user combobox of channel members + DM partner +// channel combobox of joined channels +// date / time / +// datetime native date/time picker +// country select w/ flag + name; wire value is ISO-2 code +// password masked text input +// * with choices[] select of the bot-declared choices +// +// On submit the modal builds an options-map and calls sendBotCommand +// directly -- bypassing the freeform-args parser in useMessageSending. + +import { Trans, t } from "@lingui/macro"; +import type React from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { sendBotCommand } from "../../hooks/useMessageSending"; +import { COUNTRIES, flagEmoji } from "../../lib/countries"; +import type { + BotCommand, + BotCommandOption, + Channel, + PrivateChat, +} from "../../types"; + +interface Props { + serverId: string; + botNick: string; + command: BotCommand; + channel: Channel | null; + privateChat: PrivateChat | null; + /** Members from the active channel so the "user" type can + * autocomplete against people who are reachable right now. */ + channelMembers: string[]; + /** Channel names the user has joined for the "channel" type. */ + joinedChannels: string[]; + onClose: () => void; +} + +export const SlashCommandParamModal: React.FC = ({ + serverId, + botNick, + command, + channel, + privateChat, + channelMembers, + joinedChannels, + onClose, +}) => { + const opts = command.options ?? []; + const [values, setValues] = useState< + Record + >(() => { + const init: Record = {}; + for (const o of opts) { + if (o.type === "bool") init[o.name] = false; + else if (o.type === "int" || o.type === "number") init[o.name] = ""; + else init[o.name] = ""; + } + return init; + }); + const [error, setError] = useState(null); + const firstFieldRef = useRef( + null, + ); + + useEffect(() => { + firstFieldRef.current?.focus(); + }, []); + + // Allow Escape to dismiss; Enter on a non-textarea submits. + const onKey = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.stopPropagation(); + onClose(); + } + }, + [onClose], + ); + useEffect(() => { + document.addEventListener("keydown", onKey, true); + return () => document.removeEventListener("keydown", onKey, true); + }, [onKey]); + + const setVal = (name: string, v: string | number | boolean) => + setValues((prev) => ({ ...prev, [name]: v })); + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + // Validate required + coerce numerics; drop empty optional fields + // so the bot sees them as absent rather than empty string. + const payload: Record = {}; + for (const o of opts) { + const raw = values[o.name]; + const empty = + raw === "" || raw === undefined || raw === null || raw === false; + if (o.required && empty && o.type !== "bool") { + setError(t`${o.name} is required.`); + return; + } + if (raw === "" || raw === undefined || raw === null) continue; + if (o.type === "int") { + const n = Number.parseInt(String(raw), 10); + if (Number.isNaN(n)) { + setError(t`${o.name} must be a whole number.`); + return; + } + payload[o.name] = n; + } else if (o.type === "number") { + const n = Number.parseFloat(String(raw)); + if (Number.isNaN(n)) { + setError(t`${o.name} must be a number.`); + return; + } + payload[o.name] = n; + } else if (o.type === "bool") { + payload[o.name] = Boolean(raw); + } else { + payload[o.name] = String(raw); + } + } + sendBotCommand(serverId, channel, botNick, command, payload); + onClose(); + }; + + return ( +
+
+
+
+

+ /{command.name} + + @{botNick} + +

+ {command.description && ( +

+ {command.description} +

+ )} +
+ +
+
+ {opts.map((o, idx) => ( + setVal(o.name, v)} + channelMembers={channelMembers} + joinedChannels={joinedChannels} + privateChatNick={privateChat?.username} + autoFocusRef={idx === 0 ? firstFieldRef : undefined} + /> + ))} + {opts.length === 0 && ( +

+ This command takes no parameters. +

+ )} +
+ {error && ( +

{error}

+ )} +
+ + +
+
+
+ ); +}; + +interface ParamFieldProps { + option: BotCommandOption; + value: string | number | boolean; + setValue: (v: string | number | boolean) => void; + channelMembers: string[]; + joinedChannels: string[]; + privateChatNick: string | undefined; + autoFocusRef?: + | React.RefObject + | undefined; +} + +const BASE_INPUT = + "w-full px-3 py-2 rounded bg-discord-dark-400 text-white text-sm placeholder:text-discord-text-muted/60 focus:outline-none focus:ring-1 focus:ring-discord-primary"; + +const ParamField: React.FC = ({ + option, + value, + setValue, + channelMembers, + joinedChannels, + privateChatNick, + autoFocusRef, +}) => { + // Bot-declared `choices` override the type renderer with a select. + const renderChoices = useMemo(() => { + if (!option.choices?.length) return null; + return ( + + ); + }, [option.choices, value, setValue, autoFocusRef]); + + let field: React.ReactNode; + if (renderChoices) { + field = renderChoices; + } else { + switch (option.type) { + case "bool": + field = ( + + ); + break; + case "int": + field = ( + } + type="number" + step="1" + value={String(value ?? "")} + onChange={(e) => setValue(e.target.value)} + className={BASE_INPUT} + /> + ); + break; + case "number": + field = ( + } + type="number" + step="any" + value={String(value ?? "")} + onChange={(e) => setValue(e.target.value)} + className={BASE_INPUT} + /> + ); + break; + case "date": + field = ( + } + type="date" + value={String(value ?? "")} + onChange={(e) => setValue(e.target.value)} + className={BASE_INPUT} + /> + ); + break; + case "time": + field = ( + } + type="time" + value={String(value ?? "")} + onChange={(e) => setValue(e.target.value)} + className={BASE_INPUT} + /> + ); + break; + case "datetime": + field = ( + } + type="datetime-local" + value={String(value ?? "")} + onChange={(e) => setValue(e.target.value)} + className={BASE_INPUT} + /> + ); + break; + case "password": + field = ( + } + type="password" + value={String(value ?? "")} + onChange={(e) => setValue(e.target.value)} + className={BASE_INPUT} + autoComplete="new-password" + /> + ); + break; + case "country": + field = ( + + ); + break; + case "user": { + const choices = Array.from( + new Set( + [...channelMembers, ...(privateChatNick ? [privateChatNick] : [])] + .filter(Boolean) + .sort((a, b) => a.localeCompare(b)), + ), + ); + const listId = `param-users-${option.name}`; + field = ( + <> + } + type="text" + value={String(value ?? "")} + onChange={(e) => setValue(e.target.value)} + className={BASE_INPUT} + placeholder="nick" + list={listId} + autoComplete="off" + /> + + {choices.map((u) => ( + + + ); + break; + } + case "channel": { + const listId = `param-channels-${option.name}`; + field = ( + <> + } + type="text" + value={String(value ?? "")} + onChange={(e) => setValue(e.target.value)} + className={BASE_INPUT} + placeholder="#channel" + list={listId} + autoComplete="off" + /> + + {joinedChannels.map((c) => ( + + + ); + break; + } + default: + field = ( + } + type="text" + value={String(value ?? "")} + onChange={(e) => setValue(e.target.value)} + className={BASE_INPUT} + /> + ); + } + } + + return ( +
+ + {field} + {option.description && option.type !== "bool" && ( +

+ {option.description} +

+ )} +
+ ); +}; + +export default SlashCommandParamModal; diff --git a/src/components/ui/SlashCommandPopover.tsx b/src/components/ui/SlashCommandPopover.tsx index 42d28568..8148ab90 100644 --- a/src/components/ui/SlashCommandPopover.tsx +++ b/src/components/ui/SlashCommandPopover.tsx @@ -1,9 +1,19 @@ // Slash-command suggestion popover, anchored above the chat input. // -// Activates when the input value starts with "/" and the user is still -// typing the command name (no space yet). Filters the server's -// cmdsAvailable set (populated by the obsidianirc/cmdslist cap) by -// prefix match. +// Three sources feed into this: +// * client → commands handled locally by the React app before they +// touch the wire (e.g. /me, /msg, /nick). Defined in +// src/lib/clientCommands.ts; rendered with a "client" badge. +// * server → obsidianirc/cmdslist capability — the IRCd's set of +// commands the user is currently permitted to invoke (e.g. /op, +// /kick, /mode); rendered with a "server" badge. +// * bot → draft/bot-cmds — per-bot schemas with descriptions, +// options, scope. "channel-bot" or "server-bot" badge. +// +// Below the command name we show the description and a `` / +// `[optional]` parameter signature. Once the user accepts a +// suggestion and starts typing arguments, the popover yields to the +// param-hint footer (see SlashParamHint). // // Keyboard: // ArrowUp / ArrowDown -- cycle highlighted suggestion @@ -14,13 +24,26 @@ import type React from "react"; import { useEffect, useMemo, useRef, useState } from "react"; +import type { BotCommandOption } from "../../types"; + +export type SlashSuggestionSource = + | { kind: "client" } + | { kind: "server" } + | { kind: "bot"; botNick: string; scope?: "channel" | "server" }; + +export interface SlashSuggestion { + name: string; + description?: string; + options?: BotCommandOption[]; + source: SlashSuggestionSource; +} interface SlashCommandPopoverProps { isVisible: boolean; inputValue: string; - commands: string[]; + commands: SlashSuggestion[]; inputElement?: HTMLInputElement | HTMLTextAreaElement | null; - onSelect: (command: string) => void; + onSelect: (suggestion: SlashSuggestion) => void; onClose: () => void; } @@ -40,6 +63,68 @@ export function getActiveSlashQuery( return beforeCursor.slice(1).toLowerCase(); } +/** Compact "name [optional]" rendering of an option list. */ +export function formatOptions(options: BotCommandOption[] | undefined): string { + if (!options || options.length === 0) return ""; + return options + .map((o) => (o.required ? `<${o.name}>` : `[${o.name}]`)) + .join(" "); +} + +interface BadgeStyle { + label: string; + title: string; + className: string; +} + +function badgeStyle(source: SlashSuggestionSource): BadgeStyle { + switch (source.kind) { + case "client": + return { + label: "client", + title: "Handled by ObsidianIRC before being sent", + className: + "bg-discord-dark-200 text-discord-text-muted border border-discord-dark-500", + }; + case "server": + return { + label: "server", + title: "Command provided by the IRC server", + className: + "bg-emerald-700/40 text-emerald-300 border border-emerald-600/60", + }; + case "bot": + return source.scope === "server" + ? { + // Sky instead of brand purple: the selected-row highlight + // is bg-discord-primary, and the old purple badge tint + // disappeared against it. Sky still reads as "network- + // wide service" and stays legible on both backgrounds. + label: "server-bot", + title: "Server-wide bot — reachable from any channel", + className: "bg-sky-700/40 text-sky-300 border border-sky-600/60", + } + : { + label: "channel-bot", + title: "Channel bot — present in this channel", + className: + "bg-amber-700/30 text-amber-300 border border-amber-600/50", + }; + } +} + +function sourceBadge(source: SlashSuggestionSource): React.ReactNode { + const { label, title, className } = badgeStyle(source); + return ( + + {label} + + ); +} + export const SlashCommandPopover: React.FC = ({ isVisible, inputValue, @@ -54,10 +139,10 @@ export const SlashCommandPopover: React.FC = ({ const ref = useRef(null); const matches = useMemo(() => { - if (query === null) return [] as string[]; + if (query === null) return [] as SlashSuggestion[]; if (commands.length === 0) return []; return commands - .filter((c) => c.startsWith(query)) + .filter((c) => c.name.toLowerCase().startsWith(query)) .slice(0, MAX_SUGGESTIONS); }, [commands, query]); @@ -100,38 +185,76 @@ export const SlashCommandPopover: React.FC = ({ if (!isVisible || query === null || matches.length === 0) return null; - // Position above the input box, left-aligned. + // Pin the popover's bottom edge to the input's top edge (small 6px + // gap) and its left edge to the input's left edge. Using `bottom` + // instead of `top + computed height` means the popover stays + // correctly anchored regardless of how many rows it currently has + // or whether any rows carry a description -- no row-height estimate + // needed. const inputRect = inputElement?.getBoundingClientRect(); - const top = inputRect - ? inputRect.top + window.scrollY - matches.length * 32 - 32 - : 100; - const left = inputRect ? inputRect.left + window.scrollX : 100; + // Refuse to render until the input has a real position on screen -- + // otherwise the first frame shows the popover anchored at viewport + // (100, 100) for a flash before the ref resolves and it snaps to + // the input. + if (!inputRect || inputRect.height === 0) return null; + const bottom = window.innerHeight - inputRect.top + 6; + const left = inputRect.left; return (
-
+
Slash commands
- {matches.map((cmd, index) => ( -
onSelect(cmd)} - onMouseEnter={() => setSelectedIndex(index)} - > - /{cmd} -
- ))} + {matches.map((cmd, index) => { + const sig = formatOptions(cmd.options); + const isSelected = index === selectedIndex; + return ( +
onSelect(cmd)} + onMouseEnter={() => setSelectedIndex(index)} + > +
+ + /{cmd.name} + {sig && ( + + {sig} + + )} + + {sourceBadge(cmd.source)} + {cmd.source.kind === "bot" && ( + + @{cmd.source.botNick} + + )} +
+ {cmd.description && ( +
+ {cmd.description} +
+ )} +
+ ); + })}
); diff --git a/src/components/ui/SlashParamHint.tsx b/src/components/ui/SlashParamHint.tsx new file mode 100644 index 00000000..303be876 --- /dev/null +++ b/src/components/ui/SlashParamHint.tsx @@ -0,0 +1,187 @@ +// Inline hint shown above the chat input once the user has typed the +// command name and at least one space, e.g. +// +// /forecast lon +// ─────────────────────────────────────────────────── +// /forecast — Look up the current weather for a city +// ^^^ city (string, required) via @weather +// +// The active parameter (the one the cursor is currently on) is bolded. +// Only fires for draft/bot-cmds commands -- builtin /op /me etc. don't +// publish a schema so there's nothing to hint about. + +import { Trans } from "@lingui/react/macro"; +import type React from "react"; +import { useEffect, useMemo, useState } from "react"; +import type { BotCommand } from "../../types"; + +export interface SlashParamSchema { + command: BotCommand; + source: "client" | "bot"; + /** Present iff source === "bot" */ + botNick?: string; + scope: "channel" | "server" | "dm"; +} + +interface SlashParamHintProps { + inputValue: string; + cursorPosition: number; + /** Map of command name → schema, lowercased keys. */ + schemas: Record; + inputElement?: HTMLInputElement | HTMLTextAreaElement | null; +} + +/** Returns { cmdName, argIndex } when the cursor is inside an arg + * position of `/ …`, otherwise null. + * + * cmdName is the raw form (may include `@botnick`); the caller can + * look this up directly against a schemas map keyed by the same + * composite form, then fall back to the bare cmd if no specific + * entry exists. */ +export function getActiveParamContext( + input: string, + cursor: number, +): { cmdName: string; argIndex: number } | null { + if (!input.startsWith("/") || input.startsWith("//")) return null; + // Strip leading slash, find the command name (before first space). + const head = input.slice(1); + const firstSpace = head.indexOf(" "); + if (firstSpace === -1) return null; // still typing the command name + const cmdName = head.slice(0, firstSpace).toLowerCase(); + const bare = cmdName.includes("@") ? cmdName.split("@")[0] : cmdName; + if (!bare) return null; + + // Cursor must be at or past the first space. + const cursorInHead = cursor - 1; + if (cursorInHead <= firstSpace) return null; + + // Count spaces in head[0..cursorInHead] to figure out which arg. + let argIndex = -1; // -1 means still in cmd name region + for (let i = 0; i <= cursorInHead && i < head.length; i++) { + if (head[i] === " ") argIndex++; + } + if (argIndex < 0) return null; + return { cmdName, argIndex }; +} + +export const SlashParamHint: React.FC = ({ + inputValue, + cursorPosition, + schemas, + inputElement, +}) => { + // The parent only re-renders this hint when its own state changes + // (and it intentionally avoids re-rendering on every keystroke for + // perf reasons -- input value is held in a ref). Subscribe to the + // input element directly so the hint re-evaluates context on every + // edit; only then will it correctly disappear when the user edits + // the command name to something the schemas don't know about. + const [liveValue, setLiveValue] = useState(inputValue); + const [liveCursor, setLiveCursor] = useState(cursorPosition); + + useEffect(() => { + if (!inputElement) return; + const refresh = () => { + setLiveValue(inputElement.value); + setLiveCursor(inputElement.selectionStart ?? inputElement.value.length); + }; + refresh(); + inputElement.addEventListener("input", refresh); + inputElement.addEventListener("keyup", refresh); + inputElement.addEventListener("click", refresh); + return () => { + inputElement.removeEventListener("input", refresh); + inputElement.removeEventListener("keyup", refresh); + inputElement.removeEventListener("click", refresh); + }; + }, [inputElement]); + + // Keep parent-provided props as a fallback for the first render + // before the listener has fired (and so unit tests that don't wire + // an element still get the static read). + const effValue = inputElement ? liveValue : inputValue; + const effCursor = inputElement ? liveCursor : cursorPosition; + + const ctx = useMemo( + () => getActiveParamContext(effValue, effCursor), + [effValue, effCursor], + ); + + if (!ctx) return null; + // Try `/cmd@bot` first, then fall back to the bare command name. + const bare = ctx.cmdName.includes("@") + ? ctx.cmdName.split("@")[0] + : ctx.cmdName; + const entry = schemas[ctx.cmdName] ?? schemas[bare]; + if (!entry) return null; + + const opts = entry.command.options ?? []; + if (opts.length === 0) return null; + + // Position above the input, same anchor as the popover. + const inputRect = inputElement?.getBoundingClientRect(); + const top = inputRect ? inputRect.top + window.scrollY - 70 : 100; + const left = inputRect ? inputRect.left + window.scrollX : 100; + + return ( +
+
+ /{entry.command.name}{" "} + {opts.map((o, i) => { + const active = i === ctx.argIndex; + const text = o.required ? `<${o.name}>` : `[${o.name}]`; + return ( + + {text}{" "} + + ); + })} + + {entry.source === "bot" ? ( + via @{entry.botNick} + ) : ( + (handled by ObsidianIRC) + )} + +
+ {opts[ctx.argIndex] && ( +
+ + {opts[ctx.argIndex].name} + + {" — "} + {opts[ctx.argIndex].type || "string"} + {opts[ctx.argIndex].required && ( + + required + + )} + {opts[ctx.argIndex].description && ( + — {opts[ctx.argIndex].description} + )} +
+ )} + {/* show choices if present */} + {(opts[ctx.argIndex]?.choices?.length ?? 0) > 0 && ( +
+ one of:{" "} + + {opts[ctx.argIndex].choices?.join(", ")} + +
+ )} +
+ ); +}; + +export default SlashParamHint; diff --git a/src/hooks/useMessageSending.ts b/src/hooks/useMessageSending.ts index 38dc26a0..9d81b965 100644 --- a/src/hooks/useMessageSending.ts +++ b/src/hooks/useMessageSending.ts @@ -4,6 +4,7 @@ */ import { useCallback } from "react"; import { v4 as uuidv4 } from "uuid"; +import { base64EncodeUtf8 } from "../lib/base64"; import ircClient from "../lib/ircClient"; import { makeLabel, withLabel } from "../lib/labeledResponse"; import { @@ -12,7 +13,153 @@ import { } from "../lib/messageFormatter"; import { createBatchId, splitLongMessage } from "../lib/messageProtocol"; import useStore, { serverSupportsMultiline } from "../store"; -import type { Channel, Message, PrivateChat, User } from "../types"; +import type { BotCommand, Channel, Message, PrivateChat, User } from "../types"; + +/** + * Try to dispatch a slash command as a +draft/bot-cmd TAGMSG. + * Returns true if a matching bot command was found and the TAGMSG + * was sent. Resolution order: + * 1) explicit `/cmd@botnick` syntax targets one bot + * 2) otherwise scan bots in the current channel for a matching name + * 3) DM target matches if its nick is a bot + * 4) server-wide bots (no channel) fall through last + */ +function tryDispatchBotCommand( + serverId: string, + channel: Channel | null, + privateChat: PrivateChat | null, + rawCmdName: string, + args: string[], +): boolean { + const server = useStore.getState().servers.find((s) => s.id === serverId); + if (!server?.botCommands) return false; + const bots = server.botCommands; + let target = rawCmdName; + let cmdName = rawCmdName; + if (rawCmdName.includes("@")) { + const [c, t] = rawCmdName.split("@", 2); + cmdName = c; + target = t; + } else { + target = ""; + } + const lowerCmd = cmdName.toLowerCase(); + + type Match = { bot: string; cmd: BotCommand }; + const matches: Match[] = []; + // explicit target via /cmd@botnick + if (target) { + const list = bots[target.toLowerCase()]; + if (list) { + const cmd = list.find((c) => c.name.toLowerCase() === lowerCmd); + if (cmd) matches.push({ bot: target, cmd }); + } + } + // channel-bot search: any bot we know AND who's in the channel + if (!matches.length && channel) { + const nicksInChannel = new Set( + channel.users.map((u) => u.username.toLowerCase()), + ); + for (const [bot, list] of Object.entries(bots)) { + if (!nicksInChannel.has(bot)) continue; + const cmd = list.find((c) => c.name.toLowerCase() === lowerCmd); + if (cmd) matches.push({ bot, cmd }); + } + } + // DM with a bot + if (!matches.length && privateChat) { + const list = bots[privateChat.username.toLowerCase()]; + if (list) { + const cmd = list.find((c) => c.name.toLowerCase() === lowerCmd); + if (cmd) matches.push({ bot: privateChat.username, cmd }); + } + } + // server-wide bots (any bot we know that defines the command) + if (!matches.length) { + for (const [bot, list] of Object.entries(bots)) { + const cmd = list.find((c) => c.name.toLowerCase() === lowerCmd); + if (cmd) matches.push({ bot, cmd }); + } + } + if (!matches.length) return false; + // First match wins (channel-scope already preferred over server-scope + // by virtue of the lookup ordering above). + const { bot, cmd } = matches[0]; + + // Naive arg parsing: map positional args onto declared options in + // order, leftovers concatenated onto the last string-typed option. + const options: Record = {}; + const opts = cmd.options ?? []; + for (let i = 0; i < opts.length && i < args.length; i++) { + const o = opts[i]; + const isLast = i === opts.length - 1; + const raw = isLast ? args.slice(i).join(" ") : args[i]; + if (o.type === "int") options[o.name] = Number.parseInt(raw, 10); + else if (o.type === "bool") + options[o.name] = raw === "true" || raw === "1" || raw === "yes"; + else options[o.name] = raw; + } + + sendBotCommand(serverId, channel, bot, cmd, options); + return true; +} + +/** + * Wire-level send for a pre-resolved bot command with structured + * options (no freeform-arg parsing). Used both by the freeform-text + * path above and by the param modal, which collects values directly + * via form controls. + */ +// Where a command may be invoked. Prefer the spec `contexts`; fall back to the +// legacy obby.world/bot-info `visibility`/`scopes` pair so a private command is +// NEVER routed publicly just because the directory hasn't sent `contexts` yet. +function resolveContexts(cmd: BotCommand): ("public" | "private" | "pm")[] { + if (Array.isArray(cmd.contexts) && cmd.contexts.length > 0) + return cmd.contexts; + const scopes = cmd.scopes ?? ["channel"]; + const priv = cmd.visibility === "private"; + const out: ("public" | "private" | "pm")[] = []; + if (scopes.includes("channel")) out.push(priv ? "private" : "public"); + if (scopes.includes("dm")) out.push("pm"); + return out.length > 0 ? out : [priv ? "private" : "public"]; +} + +export function sendBotCommand( + serverId: string, + channel: Channel | null, + bot: string, + cmd: BotCommand, + options: Record, +): void { + const contexts = resolveContexts(cmd); + const canPublic = contexts.includes("public"); + const canPrivate = contexts.includes("private"); + const canPm = contexts.includes("pm"); + + // Per spec §Invoking a command: the base64 of the compact JSON invocation + // rides in +draft/bot-cmd. How it is addressed depends on the context. + if (channel && canPublic) { + // public: TAGMSG to the channel. Name the target bot so that when several + // bots in the channel share a command name, only the intended one acts. + const payload = { name: cmd.name, options, bot }; + const b64 = base64EncodeUtf8(JSON.stringify(payload)); + ircClient.sendRaw( + serverId, + `@+draft/bot-cmd=${b64} TAGMSG ${channel.name}`, + ); + } else if (channel && canPrivate) { + // private: TAGMSG to the bot. The channel travels in the payload, since + // +draft/channel-context is not valid on TAGMSG. + const payload = { name: cmd.name, options, channel: channel.name }; + const b64 = base64EncodeUtf8(JSON.stringify(payload)); + ircClient.sendRaw(serverId, `@+draft/bot-cmd=${b64} TAGMSG ${bot}`); + } else if (canPm) { + // pm: TAGMSG to the bot, no channel. + const payload = { name: cmd.name, options }; + const b64 = base64EncodeUtf8(JSON.stringify(payload)); + ircClient.sendRaw(serverId, `@+draft/bot-cmd=${b64} TAGMSG ${bot}`); + } +} /** * labeled-response is only useful when the server will also echo our @@ -231,6 +378,16 @@ export function useMessageSending({ } } else if (commandName === "back") { clearAway(selectedServerId); + } else if ( + tryDispatchBotCommand( + selectedServerId, + selectedChannel, + selectedPrivateChat, + commandName, + args, + ) + ) { + // bot-cmd dispatched, nothing else to do } else { const fullCommand = args.length > 0 ? `${commandName} ${args.join(" ")}` : commandName; diff --git a/src/lib/base64.ts b/src/lib/base64.ts new file mode 100644 index 00000000..0c2073b1 --- /dev/null +++ b/src/lib/base64.ts @@ -0,0 +1,20 @@ +// UTF-8-safe base64 with padding (RFC 4648 §4), shared by the draft/bot-cmds +// and draft/bot-tools protocols. btoa/atob operate on Latin-1 only, so we +// round-trip through UTF-8 bytes; the base64 alphabet never collides with +// IRCv3 tag-value escaping, so no escape pass is needed on the wire. + +export function base64EncodeUtf8(s: string): string { + const bytes = new TextEncoder().encode(s); + let bin = ""; + for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); + return btoa(bin); +} + +export function base64DecodeUtf8(b64: string): string { + // atob tolerates missing padding; normalise so senders that strip `=` work. + const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4); + const bin = atob(padded); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + return new TextDecoder().decode(bytes); +} diff --git a/src/lib/botTools.ts b/src/lib/botTools.ts new file mode 100644 index 00000000..1282cbf9 --- /dev/null +++ b/src/lib/botTools.ts @@ -0,0 +1,257 @@ +// draft/bot-tools — IRCv3 "Bot Tools" workflow transparency. +// +// All workflow state rides in a single client-only tag whose value is the +// base64 (RFC 4648 §4, with padding) of the compact JSON body. base64 is used +// because its alphabet never collides with IRCv3 tag-value escaping, so no +// escape pass is needed on the wire in either direction. +// +// The tag name is fixed across all message kinds. The discriminator lives +// in the JSON body as the `msg` field. + +import { base64DecodeUtf8, base64EncodeUtf8 } from "./base64"; + +export const BOT_TOOLS_TAG = "+draft/bot-tools"; +export const BOT_TOOLS_CAP = "draft/bot-tools"; + +export type AiWorkflowState = + | "start" + | "reasoning" + | "running" + | "complete" + | "failed" + | "cancelled"; + +// Behaviours a bot advertises on its workflow `start` message so a client can +// show the right controls before any step arrives. +export type AiWorkflowFeature = "interactive" | "reasoning" | "approval"; + +export type AiStepType = "reasoning" | "tool-call" | "tool-result" | "text"; + +export type AiStepState = + | "start" + | "running" + | "pending-approval" + | "complete" + | "failed" + | "cancelled"; + +export type AiActionType = "cancel" | "approve" | "reject" | "input"; + +export interface AiWorkflowMessage { + msg: "workflow"; + id: string; + state: AiWorkflowState; + name?: string; + trigger?: string; + features?: AiWorkflowFeature[]; + // Short truncated copy of the prompt that started the workflow. A bot-neutral + // hint, not part of the spec: lets a client show "Answering : + // " inline on the workflow card without scrolling back to the + // trigger message. Decoders ignore it if absent. + prompt?: string; + "cancelled-by"?: string; +} + +export interface AiStepMessage { + msg: "step"; + wid: string; + sid: string; + type: AiStepType; + state: AiStepState; + tool?: string; + label?: string; + // For tool-call: nested JSON object of arguments. Other types: string fragment. + content?: unknown; + truncated?: boolean; + "cancelled-by"?: string; +} + +export interface AiActionMessage { + msg: "action"; + action: AiActionType; + target: string; + content?: string; +} + +export type BotToolsMessage = + | AiWorkflowMessage + | AiStepMessage + | AiActionMessage; + +const WORKFLOW_STATES: ReadonlySet = new Set([ + "start", + "reasoning", + "running", + "complete", + "failed", + "cancelled", +]); +const STEP_TYPES: ReadonlySet = new Set([ + "reasoning", + "tool-call", + "tool-result", + "text", +]); +const STEP_STATES: ReadonlySet = new Set([ + "start", + "running", + "pending-approval", + "complete", + "failed", + "cancelled", +]); +const ACTION_TYPES: ReadonlySet = new Set([ + "cancel", + "approve", + "reject", + "input", +]); + +// Decode a raw tag value (base64 of compact JSON) into a structured message. +// Returns null on any decode/parse failure or schema mismatch rather than +// throwing, per spec §Security: malformed or oversized payloads are silently +// discarded. +export function decodeBotToolsValue(raw: string): BotToolsMessage | null { + if (!raw) return null; + let parsed: unknown; + try { + parsed = JSON.parse(base64DecodeUtf8(raw)); + } catch { + return null; + } + if (!parsed || typeof parsed !== "object") return null; + const obj = parsed as Record; + + switch (obj.msg) { + case "workflow": { + if ( + typeof obj.id !== "string" || + typeof obj.state !== "string" || + !WORKFLOW_STATES.has(obj.state as AiWorkflowState) + ) + return null; + const m: AiWorkflowMessage = { + msg: "workflow", + id: obj.id, + state: obj.state as AiWorkflowState, + }; + if (typeof obj.name === "string") m.name = obj.name; + if (typeof obj.trigger === "string") m.trigger = obj.trigger; + if (Array.isArray(obj.features)) + m.features = obj.features.filter( + (f): f is AiWorkflowFeature => typeof f === "string", + ) as AiWorkflowFeature[]; + if (typeof obj.prompt === "string") m.prompt = obj.prompt; + if (typeof obj["cancelled-by"] === "string") + m["cancelled-by"] = obj["cancelled-by"] as string; + return m; + } + case "step": { + if ( + typeof obj.wid !== "string" || + typeof obj.sid !== "string" || + typeof obj.type !== "string" || + typeof obj.state !== "string" || + !STEP_TYPES.has(obj.type as AiStepType) || + !STEP_STATES.has(obj.state as AiStepState) + ) + return null; + const m: AiStepMessage = { + msg: "step", + wid: obj.wid, + sid: obj.sid, + type: obj.type as AiStepType, + state: obj.state as AiStepState, + }; + if (typeof obj.tool === "string") m.tool = obj.tool; + if (typeof obj.label === "string") m.label = obj.label; + if (obj.content !== undefined) m.content = obj.content; + if (typeof obj.truncated === "boolean") m.truncated = obj.truncated; + if (typeof obj["cancelled-by"] === "string") + m["cancelled-by"] = obj["cancelled-by"] as string; + return m; + } + case "action": { + if ( + typeof obj.action !== "string" || + typeof obj.target !== "string" || + !ACTION_TYPES.has(obj.action as AiActionType) + ) + return null; + const m: AiActionMessage = { + msg: "action", + action: obj.action as AiActionType, + target: obj.target, + }; + if (typeof obj.content === "string") m.content = obj.content; + return m; + } + default: + return null; + } +} + +// base64 of compact JSON (no whitespace), per spec §Value Encoding. +export function encodeBotToolsValue(msg: BotToolsMessage): string { + return base64EncodeUtf8(JSON.stringify(msg)); +} + +// User-facing step count: "reasoning" frames don't count (they're the +// bot planning, not work), and a tool-call + matching tool-result +// pair counts as a single step (they're the two sides of one tool +// invocation, paired FIFO by tool name). +export function countableSteps( + steps: readonly { type: string; tool?: string }[], +): number { + const paired = new Set(); + let count = 0; + for (let i = 0; i < steps.length; i++) { + if (paired.has(i)) continue; + const s = steps[i]; + if (s.type === "reasoning") continue; + if (s.type === "tool-call") { + for (let j = i + 1; j < steps.length; j++) { + if (paired.has(j)) continue; + const t = steps[j]; + if (t.type === "tool-result" && t.tool === s.tool) { + paired.add(j); + break; + } + } + count++; + continue; + } + count++; + } + return count; +} + +// IRC tag-value escape — applied just before putting the value on the +// wire. Mirrors the unescape in src/lib/ircUtils.tsx. +export function escapeIrcTagValue(s: string): string { + let out = ""; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + switch (c) { + case "\\": + out += "\\\\"; + break; + case ";": + out += "\\:"; + break; + case " ": + out += "\\s"; + break; + case "\r": + out += "\\r"; + break; + case "\n": + out += "\\n"; + break; + default: + out += c; + break; + } + } + return out; +} diff --git a/src/lib/clientCommands.ts b/src/lib/clientCommands.ts new file mode 100644 index 00000000..d1c3c1df --- /dev/null +++ b/src/lib/clientCommands.ts @@ -0,0 +1,134 @@ +// Canonical list of slash commands the client itself handles before +// (or instead of) sending them on the wire. Kept centralized so the +// suggestion popover (src/components/ui/SlashCommandPopover.tsx) and +// the dispatcher (src/hooks/useMessageSending.ts) can't drift apart. +// +// Each entry mirrors the fields a draft/bot-cmds schema would publish, +// so the popover + param-hint can render them with the same code path +// as PushBot commands. +// +// To add a new client-only command: +// 1. add an entry below +// 2. add the matching `commandName === "..."` branch to handleCommand +// in src/hooks/useMessageSending.ts +// 3. that's it — the popover and param hint pick it up automatically + +import { t } from "@lingui/core/macro"; +import type { BotCommandOption } from "../types"; + +export interface ClientCommand { + name: string; + description: string; + options?: BotCommandOption[]; + /** Where the command makes sense. Channel-only commands won't + * show in DM views. */ + scope?: "anywhere" | "channel-only"; +} + +// Function rather than module-scope const: `t` evaluates eagerly, and +// at import time the i18n catalogue hasn't been activated yet, so a +// module-level `t\`...\`` would freeze in the source-locale string. +// Callers re-invoke this on every render anyway (it's the popover/dispatcher +// reading the live list), so the per-call allocation is fine. +export function getClientCommands(): ClientCommand[] { + return [ + { + name: "me", + description: t`Send an action / emote`, + options: [ + { + name: "action", + type: "string", + required: true, + description: t`What you're doing`, + }, + ], + }, + { + name: "msg", + description: t`Open a private message to a user`, + options: [ + { name: "user", type: "user", required: true }, + { + name: "message", + type: "string", + required: true, + description: t`First message to send`, + }, + ], + }, + { + name: "whisper", + description: t`Whisper to a user in the current channel context`, + scope: "channel-only", + options: [ + { name: "user", type: "user", required: true }, + { name: "message", type: "string", required: true }, + ], + }, + { + name: "join", + description: t`Join a channel`, + options: [ + { + name: "channel", + type: "channel", + required: true, + description: t`Channel to join (#name)`, + }, + ], + }, + { + name: "part", + description: t`Leave a channel`, + options: [ + { + name: "channel", + type: "channel", + required: false, + description: t`Channel to leave (defaults to current)`, + }, + ], + }, + { + name: "nick", + description: t`Change your nickname on this server`, + options: [ + { + name: "newnick", + type: "string", + required: true, + description: t`New nickname`, + }, + ], + }, + { + name: "away", + description: t`Mark yourself as away`, + options: [ + { + name: "reason", + type: "string", + required: false, + description: t`Away message`, + }, + ], + }, + { + name: "back", + description: t`Mark yourself as back`, + }, + ]; +} + +// Name set is locale-independent so it can stay module-scope. +export const CLIENT_COMMAND_NAMES: ReadonlySet = new Set([ + "me", + "msg", + "whisper", + "join", + "part", + "nick", + "away", + "back", +]); diff --git a/src/lib/countries.ts b/src/lib/countries.ts new file mode 100644 index 00000000..5ddbbf93 --- /dev/null +++ b/src/lib/countries.ts @@ -0,0 +1,269 @@ +// ISO 3166-1 alpha-2 country codes for the "country" option type +// in the slash-command param modal. Names are English short-form +// per the standard; bots receive the alpha-2 code as the wire value +// so they can localise the display themselves if they wish. + +export interface Country { + code: string; + name: string; +} + +/** Render a country code as its flag emoji via Unicode regional + * indicator symbols (U+1F1E6 = 'A'). */ +export function flagEmoji(code: string): string { + if (!code || code.length !== 2) return ""; + const cp = (c: string) => 0x1f1e6 + (c.toUpperCase().charCodeAt(0) - 65); + return String.fromCodePoint(cp(code[0])) + String.fromCodePoint(cp(code[1])); +} + +export const COUNTRIES: Country[] = [ + { code: "AD", name: "Andorra" }, + { code: "AE", name: "United Arab Emirates" }, + { code: "AF", name: "Afghanistan" }, + { code: "AG", name: "Antigua and Barbuda" }, + { code: "AI", name: "Anguilla" }, + { code: "AL", name: "Albania" }, + { code: "AM", name: "Armenia" }, + { code: "AO", name: "Angola" }, + { code: "AQ", name: "Antarctica" }, + { code: "AR", name: "Argentina" }, + { code: "AS", name: "American Samoa" }, + { code: "AT", name: "Austria" }, + { code: "AU", name: "Australia" }, + { code: "AW", name: "Aruba" }, + { code: "AX", name: "Åland Islands" }, + { code: "AZ", name: "Azerbaijan" }, + { code: "BA", name: "Bosnia and Herzegovina" }, + { code: "BB", name: "Barbados" }, + { code: "BD", name: "Bangladesh" }, + { code: "BE", name: "Belgium" }, + { code: "BF", name: "Burkina Faso" }, + { code: "BG", name: "Bulgaria" }, + { code: "BH", name: "Bahrain" }, + { code: "BI", name: "Burundi" }, + { code: "BJ", name: "Benin" }, + { code: "BL", name: "Saint Barthélemy" }, + { code: "BM", name: "Bermuda" }, + { code: "BN", name: "Brunei Darussalam" }, + { code: "BO", name: "Bolivia" }, + { code: "BQ", name: "Bonaire, Sint Eustatius and Saba" }, + { code: "BR", name: "Brazil" }, + { code: "BS", name: "Bahamas" }, + { code: "BT", name: "Bhutan" }, + { code: "BV", name: "Bouvet Island" }, + { code: "BW", name: "Botswana" }, + { code: "BY", name: "Belarus" }, + { code: "BZ", name: "Belize" }, + { code: "CA", name: "Canada" }, + { code: "CC", name: "Cocos (Keeling) Islands" }, + { code: "CD", name: "Congo (Democratic Republic)" }, + { code: "CF", name: "Central African Republic" }, + { code: "CG", name: "Congo" }, + { code: "CH", name: "Switzerland" }, + { code: "CI", name: "Côte d'Ivoire" }, + { code: "CK", name: "Cook Islands" }, + { code: "CL", name: "Chile" }, + { code: "CM", name: "Cameroon" }, + { code: "CN", name: "China" }, + { code: "CO", name: "Colombia" }, + { code: "CR", name: "Costa Rica" }, + { code: "CU", name: "Cuba" }, + { code: "CV", name: "Cabo Verde" }, + { code: "CW", name: "Curaçao" }, + { code: "CX", name: "Christmas Island" }, + { code: "CY", name: "Cyprus" }, + { code: "CZ", name: "Czechia" }, + { code: "DE", name: "Germany" }, + { code: "DJ", name: "Djibouti" }, + { code: "DK", name: "Denmark" }, + { code: "DM", name: "Dominica" }, + { code: "DO", name: "Dominican Republic" }, + { code: "DZ", name: "Algeria" }, + { code: "EC", name: "Ecuador" }, + { code: "EE", name: "Estonia" }, + { code: "EG", name: "Egypt" }, + { code: "EH", name: "Western Sahara" }, + { code: "ER", name: "Eritrea" }, + { code: "ES", name: "Spain" }, + { code: "ET", name: "Ethiopia" }, + { code: "FI", name: "Finland" }, + { code: "FJ", name: "Fiji" }, + { code: "FK", name: "Falkland Islands" }, + { code: "FM", name: "Micronesia" }, + { code: "FO", name: "Faroe Islands" }, + { code: "FR", name: "France" }, + { code: "GA", name: "Gabon" }, + { code: "GB", name: "United Kingdom" }, + { code: "GD", name: "Grenada" }, + { code: "GE", name: "Georgia" }, + { code: "GF", name: "French Guiana" }, + { code: "GG", name: "Guernsey" }, + { code: "GH", name: "Ghana" }, + { code: "GI", name: "Gibraltar" }, + { code: "GL", name: "Greenland" }, + { code: "GM", name: "Gambia" }, + { code: "GN", name: "Guinea" }, + { code: "GP", name: "Guadeloupe" }, + { code: "GQ", name: "Equatorial Guinea" }, + { code: "GR", name: "Greece" }, + { code: "GS", name: "South Georgia and the South Sandwich Islands" }, + { code: "GT", name: "Guatemala" }, + { code: "GU", name: "Guam" }, + { code: "GW", name: "Guinea-Bissau" }, + { code: "GY", name: "Guyana" }, + { code: "HK", name: "Hong Kong" }, + { code: "HM", name: "Heard Island and McDonald Islands" }, + { code: "HN", name: "Honduras" }, + { code: "HR", name: "Croatia" }, + { code: "HT", name: "Haiti" }, + { code: "HU", name: "Hungary" }, + { code: "ID", name: "Indonesia" }, + { code: "IE", name: "Ireland" }, + { code: "IL", name: "Israel" }, + { code: "IM", name: "Isle of Man" }, + { code: "IN", name: "India" }, + { code: "IO", name: "British Indian Ocean Territory" }, + { code: "IQ", name: "Iraq" }, + { code: "IR", name: "Iran" }, + { code: "IS", name: "Iceland" }, + { code: "IT", name: "Italy" }, + { code: "JE", name: "Jersey" }, + { code: "JM", name: "Jamaica" }, + { code: "JO", name: "Jordan" }, + { code: "JP", name: "Japan" }, + { code: "KE", name: "Kenya" }, + { code: "KG", name: "Kyrgyzstan" }, + { code: "KH", name: "Cambodia" }, + { code: "KI", name: "Kiribati" }, + { code: "KM", name: "Comoros" }, + { code: "KN", name: "Saint Kitts and Nevis" }, + { code: "KP", name: "North Korea" }, + { code: "KR", name: "South Korea" }, + { code: "KW", name: "Kuwait" }, + { code: "KY", name: "Cayman Islands" }, + { code: "KZ", name: "Kazakhstan" }, + { code: "LA", name: "Laos" }, + { code: "LB", name: "Lebanon" }, + { code: "LC", name: "Saint Lucia" }, + { code: "LI", name: "Liechtenstein" }, + { code: "LK", name: "Sri Lanka" }, + { code: "LR", name: "Liberia" }, + { code: "LS", name: "Lesotho" }, + { code: "LT", name: "Lithuania" }, + { code: "LU", name: "Luxembourg" }, + { code: "LV", name: "Latvia" }, + { code: "LY", name: "Libya" }, + { code: "MA", name: "Morocco" }, + { code: "MC", name: "Monaco" }, + { code: "MD", name: "Moldova" }, + { code: "ME", name: "Montenegro" }, + { code: "MF", name: "Saint Martin (French part)" }, + { code: "MG", name: "Madagascar" }, + { code: "MH", name: "Marshall Islands" }, + { code: "MK", name: "North Macedonia" }, + { code: "ML", name: "Mali" }, + { code: "MM", name: "Myanmar" }, + { code: "MN", name: "Mongolia" }, + { code: "MO", name: "Macao" }, + { code: "MP", name: "Northern Mariana Islands" }, + { code: "MQ", name: "Martinique" }, + { code: "MR", name: "Mauritania" }, + { code: "MS", name: "Montserrat" }, + { code: "MT", name: "Malta" }, + { code: "MU", name: "Mauritius" }, + { code: "MV", name: "Maldives" }, + { code: "MW", name: "Malawi" }, + { code: "MX", name: "Mexico" }, + { code: "MY", name: "Malaysia" }, + { code: "MZ", name: "Mozambique" }, + { code: "NA", name: "Namibia" }, + { code: "NC", name: "New Caledonia" }, + { code: "NE", name: "Niger" }, + { code: "NF", name: "Norfolk Island" }, + { code: "NG", name: "Nigeria" }, + { code: "NI", name: "Nicaragua" }, + { code: "NL", name: "Netherlands" }, + { code: "NO", name: "Norway" }, + { code: "NP", name: "Nepal" }, + { code: "NR", name: "Nauru" }, + { code: "NU", name: "Niue" }, + { code: "NZ", name: "New Zealand" }, + { code: "OM", name: "Oman" }, + { code: "PA", name: "Panama" }, + { code: "PE", name: "Peru" }, + { code: "PF", name: "French Polynesia" }, + { code: "PG", name: "Papua New Guinea" }, + { code: "PH", name: "Philippines" }, + { code: "PK", name: "Pakistan" }, + { code: "PL", name: "Poland" }, + { code: "PM", name: "Saint Pierre and Miquelon" }, + { code: "PN", name: "Pitcairn" }, + { code: "PR", name: "Puerto Rico" }, + { code: "PS", name: "Palestine" }, + { code: "PT", name: "Portugal" }, + { code: "PW", name: "Palau" }, + { code: "PY", name: "Paraguay" }, + { code: "QA", name: "Qatar" }, + { code: "RE", name: "Réunion" }, + { code: "RO", name: "Romania" }, + { code: "RS", name: "Serbia" }, + { code: "RU", name: "Russia" }, + { code: "RW", name: "Rwanda" }, + { code: "SA", name: "Saudi Arabia" }, + { code: "SB", name: "Solomon Islands" }, + { code: "SC", name: "Seychelles" }, + { code: "SD", name: "Sudan" }, + { code: "SE", name: "Sweden" }, + { code: "SG", name: "Singapore" }, + { code: "SH", name: "Saint Helena, Ascension and Tristan da Cunha" }, + { code: "SI", name: "Slovenia" }, + { code: "SJ", name: "Svalbard and Jan Mayen" }, + { code: "SK", name: "Slovakia" }, + { code: "SL", name: "Sierra Leone" }, + { code: "SM", name: "San Marino" }, + { code: "SN", name: "Senegal" }, + { code: "SO", name: "Somalia" }, + { code: "SR", name: "Suriname" }, + { code: "SS", name: "South Sudan" }, + { code: "ST", name: "São Tomé and Príncipe" }, + { code: "SV", name: "El Salvador" }, + { code: "SX", name: "Sint Maarten (Dutch part)" }, + { code: "SY", name: "Syria" }, + { code: "SZ", name: "Eswatini" }, + { code: "TC", name: "Turks and Caicos Islands" }, + { code: "TD", name: "Chad" }, + { code: "TF", name: "French Southern Territories" }, + { code: "TG", name: "Togo" }, + { code: "TH", name: "Thailand" }, + { code: "TJ", name: "Tajikistan" }, + { code: "TK", name: "Tokelau" }, + { code: "TL", name: "Timor-Leste" }, + { code: "TM", name: "Turkmenistan" }, + { code: "TN", name: "Tunisia" }, + { code: "TO", name: "Tonga" }, + { code: "TR", name: "Türkiye" }, + { code: "TT", name: "Trinidad and Tobago" }, + { code: "TV", name: "Tuvalu" }, + { code: "TW", name: "Taiwan" }, + { code: "TZ", name: "Tanzania" }, + { code: "UA", name: "Ukraine" }, + { code: "UG", name: "Uganda" }, + { code: "UM", name: "United States Minor Outlying Islands" }, + { code: "US", name: "United States" }, + { code: "UY", name: "Uruguay" }, + { code: "UZ", name: "Uzbekistan" }, + { code: "VA", name: "Holy See (Vatican City State)" }, + { code: "VC", name: "Saint Vincent and the Grenadines" }, + { code: "VE", name: "Venezuela" }, + { code: "VG", name: "Virgin Islands (British)" }, + { code: "VI", name: "Virgin Islands (U.S.)" }, + { code: "VN", name: "Vietnam" }, + { code: "VU", name: "Vanuatu" }, + { code: "WF", name: "Wallis and Futuna" }, + { code: "WS", name: "Samoa" }, + { code: "YE", name: "Yemen" }, + { code: "YT", name: "Mayotte" }, + { code: "ZA", name: "South Africa" }, + { code: "ZM", name: "Zambia" }, + { code: "ZW", name: "Zimbabwe" }, +]; diff --git a/src/lib/irc/IRCClient.ts b/src/lib/irc/IRCClient.ts index 68cb98d5..76d05e36 100644 --- a/src/lib/irc/IRCClient.ts +++ b/src/lib/irc/IRCClient.ts @@ -609,6 +609,7 @@ export class IRCClient implements IRCClientContext { "sasl", "cap-notify", "draft/channel-rename", + "draft/bot-tools", "setname", "account-notify", "account-tag", @@ -635,6 +636,8 @@ export class IRCClient implements IRCClientContext { "labeled-response", "draft/read-marker", "obsidianirc/cmdslist", + "draft/bot-cmds", + "obby.world/channel-bots", // 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", diff --git a/src/locales/cs/messages.mjs b/src/locales/cs/messages.mjs index 6ee64297..8cbf507d 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: 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\"],\"\"],\"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! 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í: 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í! 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\"],\" 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/invitationnení 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\"],\"+fRR7i\":[\"Pozastavit\"],\"+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\"],\")\"],\"/0mUpR\":[\"Označit se jako zpět\"],\"/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\"],\"/xQ19T\":[\"Boti na této síti\"],\"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\"],\"1C/fOn\":[\"Bot zatím nezaregistroval žádné lomítkové příkazy.\"],\"1DSr1i\":[\"Zaregistrovat účet\"],\"1O/24y\":[\"Přepnout seznam kanálů\"],\"1QfxQT\":[\"Zavří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ý\"],\"1t/NnN\":[\"Zamítnout\"],\"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í\"],\"2xXP/g\":[\"Připojit se ke kanálu\"],\"2ygf/L\":[\"← Zpět\"],\"2zEgxj\":[\"Hledat GIFy...\"],\"3JjdaA\":[\"Spustit\"],\"3NJ4MW\":[\"Znovu otevřít pracovní postup, který vytvořil tuto zprávu (\",[\"stepCount\"],\" kroků)\"],\"3RdPhl\":[\"Přejmenovat kanál\"],\"3THokf\":[\"Uživatel s hlasem\"],\"3TSz9S\":[\"Minimalizovat\"],\"3et0TM\":[\"Přejít v chatu na tuto odpověď\"],\"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\"],\"4uKgKr\":[\"VÝSTUP\"],\"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 k 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:\"],\"9q17ZR\":[[\"0\"],\" je povinné.\"],\"9qIYMn\":[\"Nová přezdívka\"],\"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í?\"],\"BIDT9R\":[\"Boti\"],\"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\"],\"CaQ1Gb\":[\"Bot definovaný v konfiguraci. Pro změnu stavu upravte obbyircd.conf a použijte /REHASH.\"],\"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í\"],\"DSHF2K\":[\"Pracovní postup, který vytvořil tuto zprávu, již není ve stavu\"],\"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...\"],\"Eu7YKa\":[\"samoregistrovaný\"],\"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: 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íší...\"],\"IcHxhR\":[\"offline\"],\"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\"],\"JMXMCX\":[\"Zpráva o nepřítomnosti\"],\"JXGkhG\":[\"Změnit název kanálu (pouze operátoři)\"],\"JYiL1b\":[\"jedno z:\"],\"JcD7qf\":[\"Více akcí\"],\"JdkA+c\":[\"Tajný (+s)\"],\"Jmu12l\":[\"Kanály serveru\"],\"JvQ++s\":[\"Povolit Markdown\"],\"K2jwh/\":[\"Data WHOIS nejsou k dispozici\"],\"K4vEhk\":[\"(pozastaveno)\"],\"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\"],\"KtADxr\":[\"spustil(a)\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" krok(ů) čeká na schválení\"],\"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\\\")\"],\"LYzbQ2\":[\"Nástroj\"],\"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\"],\"M45wtf\":[\"Tento příkaz nemá žádné parametry.\"],\"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é\"],\"N40H+G\":[\"Vše\"],\"N7TQbE\":[\"Pozvat uživatele do \",[\"channelName\"]],\"NCca/o\":[\"Zadejte výchozí přezdívku...\"],\"NQN2HS\":[\"Zrušit pozastavení\"],\"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+HhhG\":[\"Šeptat uživateli v kontextu aktuálního kanálu\"],\"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\"]],\"RlCInP\":[\"Lomítkové příkazy\"],\"RySp6q\":[\"Skrýt komentáře\"],\"RzfkXn\":[\"Změnit vaši přezdívku na tomto serveru\"],\"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\"],\"TImSWn\":[\"(zpracovává ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Označit se jako nepřítomný\"],\"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í!\"],\"UqtiKk\":[\"Automatické zavření za \",[\"secondsLeft\"],\" s\"],\"UrEy4W\":[\"Otevřít soukromou zprávu uživateli\"],\"UtUIRh\":[[\"0\"],\" starších zpráv\"],\"UwzP+U\":[\"Zabezpečené připojení\"],\"V0/A4O\":[\"Vlastník kanálu\"],\"V2dwib\":[[\"0\"],\" musí být číslo.\"],\"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\"],\"XklovM\":[\"Pracuji…\"],\"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+tK3n\":[\"První zpráva k odeslání\"],\"Y/qryO\":[\"Nebyly nalezeni žádní uživatelé odpovídající vašemu vyhledávání\"],\"YAqRpI\":[\"Registrace účtu \",[\"account\"],\" proběhla úspěšně: \",[\"message\"]],\"YBXJ7j\":[\"VSTUP\"],\"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:\"],\"Z7ZXbT\":[\"Schválit\"],\"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\"],\"ZqLD8l\":[\"Pro celý server\"],\"a2/8e5\":[\"Téma nastaveno po (min)\"],\"aHKcKc\":[\"Předchozí stránka\"],\"aJTbXX\":[\"Heslo operátora\"],\"aP9gNu\":[\"výstup zkrácen\"],\"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ý\"],\"bFDO8z\":[\"gateway online\"],\"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ů.\"],\"bVBC/W\":[\"Gateway připojen\"],\"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\"],\"cde3ce\":[\"Zpráva <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\"],\"dRqrdL\":[[\"0\"],\" musí být celé číslo.\"],\"dXqxlh\":[\"<0>⚠️ Bezpečnostní riziko! 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í\"],\"dnUmOX\":[\"Na této síti zatím nejsou registrováni žádní boti.\"],\"dpCzmC\":[\"Nastavení ochrany proti floodingu\"],\"e7KzRG\":[[\"0\"],\" krok(ů)\"],\"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í: 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.\"],\"fYdEvu\":[\"Historie pracovních postupů (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Vyberte bota vlevo, abyste zobrazili jeho příkazy a možnosti správy.\"],\"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\"],\"jUXib7\":[\"Zpráva s odpovědí již není zobrazena\"],\"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:\"],\"kG2fiE\":[\"definováno v konfiguraci\"],\"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.\"],\"ky2mw7\":[\"prostřednictvím @\",[\"0\"]],\"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\"],\"lH+ed1\":[\"Čekání na první krok…\"],\"lHy8N5\":[\"Načítám více kanálů...\"],\"lasgrr\":[\"použito\"],\"lbpf14\":[\"Připojit se k \",[\"value\"]],\"lf3MT4\":[\"Kanál k opuštění (výchozí aktuální)\"],\"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)\"],\"mDkV0w\":[\"Spouštění pracovního postupu…\"],\"mEPxTp\":[\"<0>⚠️ Buďte opatrní! 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\"]],\"mHfd/S\":[\"Co děláte\"],\"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\"],\"nX4XLG\":[\"Akce operátora\"],\"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\"],\"oaTtrx\":[\"Hledat boty\"],\"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\"],\"pQBYsE\":[\"Odpovězeno v chatu\"],\"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\"],\"qXgujk\":[\"Odeslat akci / emote\"],\"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\"],\" 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\"]],\"sW5OjU\":[\"povinné\"],\"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\"],\"thC9Rq\":[\"Opustit kanál\"],\"tiBsJk\":[\"opustil \",[\"channelName\"]],\"tt4/UD\":[\"se odpojil (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Kanál k připojení (#název)\"],\"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\"]],\"uMIUx8\":[\"Smazat bota \",[\"0\"],\"? Tím se řádek v databázi měkce odstraní; přezdívku znovu použijte až po /REHASH.\"],\"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\"],\"wCKe3+\":[\"Historie pracovních postupů\"],\"wFjjxZ\":[\"byl vyhozen z \",[\"channelName\"],\" uživatelem \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nenalezeny žádné výjimky zákazu\"],\"wPrGnM\":[\"Správce kanálu\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Uvažování\"],\"wbm86v\":[\"Zobrazovat, když uživatelé vstupují nebo opouštějí kanály\"],\"wdxz7K\":[\"Zdroj\"],\"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/invitationnení 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\"],\"xeiujy\":[\"Text\"],\"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\"]],\"zPBDzU\":[\"Zrušit pracovní postup\"],\"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..772718fe 100644 --- a/src/locales/cs/messages.po +++ b/src/locales/cs/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Přinášíme IRC do budoucnosti" msgid "— open in viewer" msgstr "— otevřít v prohlížeči" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(zpracovává ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(pozastaveno)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Zobrazit 1 další položku} few {Zobrazit {1} další msgid "{0} and {1} are typing..." msgstr "{0} a {1} píší..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} je povinné." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} píše..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} musí být číslo." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} musí být celé číslo." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} starších zpráv" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} krok(ů)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} krok(ů) čeká na schválení" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Pokročilé filtry" msgid "Album" msgstr "Album" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Vše" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Veškerý obsah" @@ -297,6 +334,12 @@ msgstr "Použít filtry a obnovit" msgid "Applying..." msgstr "Používám..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Schválit" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Ověřený účet" msgid "Auto Fallback to Single Line" msgstr "Automatický návrat na jeden řádek" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Automatické zavření za {secondsLeft} s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Automaticky ověřit jako operátor při připojení" @@ -359,6 +406,10 @@ msgstr "Nepřítomen" msgid "Away from keyboard" msgstr "Pryč od klávesnice" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Zpráva o nepřítomnosti" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Zpráva o nepřítomnosti" msgid "Away:" msgstr "Nepřítomen:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Bany" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Bot zatím nezaregistroval žádné lomítkové příkazy." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Boti" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Boti na této síti" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Procházet všechny kanály na serveru" @@ -430,6 +499,7 @@ msgstr "Procházet všechny kanály na serveru" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Zrušit připojení" msgid "Cancel reply" msgstr "Zrušit odpověď" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Zrušit pracovní postup" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Změnit název kanálu (pouze operátoři)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Změnit vaši přezdívku na tomto serveru" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Změněny módy kanálu" @@ -459,6 +537,7 @@ msgstr "Změněna přezdívka" msgid "changed the topic to: {topic}" msgstr "změnil téma na: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Kanál" @@ -519,6 +598,14 @@ msgstr "Vlastník kanálu" msgid "Channel Settings" msgstr "Nastavení kanálu" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Kanál k připojení (#název)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Kanál k opuštění (výchozí aktuální)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Téma kanálu" @@ -531,6 +618,7 @@ msgstr "Kanál se nebude zobrazovat v příkazech LIST nebo NAMES" msgid "channel-name" msgstr "název-kanálu" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Kanály" @@ -606,6 +694,9 @@ msgstr "Limit klientů (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Komentáře" msgid "Comments ({commentCount})" msgstr "Komentáře ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "definováno v konfiguraci" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Bot definovaný v konfiguraci. Pro změnu stavu upravte obbyircd.conf a použijte /REHASH." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "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ů." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Výchozí přezdívka" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Smazat" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Smazat bota {0}? Tím se řádek v databázi měkce odstraní; přezdívku znovu použijte až po /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Smazat kanál" @@ -843,6 +948,10 @@ msgstr "Objevovat" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Objevte svět IRC s ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Zavřít" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Odmítnout oznámení" @@ -891,7 +1000,7 @@ 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 k nahrání" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Filtrovat kanály..." msgid "Filter members…" msgstr "Filtrovat členy…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "První zpráva k odeslání" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Profil floodingu (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (globální ban)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway připojen" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway online" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Obecné" @@ -1186,6 +1307,10 @@ msgstr "Obrázek {0} z {1}" msgid "Image preview" msgstr "Náhled obrázku" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "VSTUP" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1270,6 +1395,10 @@ msgstr "Připojit se k {0}" msgid "Join {value}" msgstr "Připojit se k {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Připojit se ke kanálu" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Zjistit více o vlastních pravidlech →" msgid "Learn more about profiles →" msgstr "Zjistit více o profilech →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Opustit kanál" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Opustit kanál" @@ -1411,6 +1544,14 @@ msgstr "Spravujte informace profilu a metadata" msgid "Mark as bot - usually 'on' or empty" msgstr "Označit jako bot - obvykle 'on' nebo prázdné" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Označit se jako nepřítomný" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Označit se jako zpět" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Max. uživatelů" @@ -1573,6 +1714,10 @@ msgstr "Název sítě" msgid "New DM" msgstr "Nová DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Nová přezdívka" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Další obrázek" @@ -1617,6 +1762,10 @@ msgstr "Nenalezeny žádné výjimky zákazu" msgid "No bans found" msgstr "Nenalezeny žádné zákazy" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Na této síti zatím nejsou registrováni žádní boti." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Bez centrálního serveru:" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - Přinášíme IRC do budoucnosti" msgid "Off" msgstr "Vypnuto" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "offline" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Offline" @@ -1754,6 +1908,10 @@ msgstr "Offline" msgid "on" msgstr "zap" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "jedno z:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Jejda! Síť se rozdělila! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Otevřít soukromou zprávu uživateli" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Otevřít nastavení konfigurace kanálu" @@ -1828,10 +1990,23 @@ msgstr "Heslo operátora" msgid "Oper Username" msgstr "Uživatelské jméno operátora" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Akce operátora" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Volitelné zprávy o pádu pro zlepšení aplikace" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "VÝSTUP" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "výstup zkrácen" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Vlastník" @@ -1994,6 +2169,10 @@ msgstr "Zpráva při odpojení" msgid "Quit the server" msgstr "Opustit server" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "spustil(a)" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "React" @@ -2024,6 +2203,10 @@ msgstr "Důvod" msgid "Reason (optional)" msgstr "Důvod (volitelné)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Uvažování" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Znovu připojit k serveru" @@ -2037,6 +2220,11 @@ msgstr "Obnovit" msgid "Register for an account" msgstr "Zaregistrovat účet" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Zamítnout" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Uvolněný" @@ -2077,11 +2265,27 @@ msgstr "Přejmenovat tento kanál na serveru. Nový název uvidí všichni uživ msgid "Render markdown formatting in messages" msgstr "Zobrazovat Markdown formátování ve zprávách" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Znovu otevřít pracovní postup, který vytvořil tuto zprávu ({stepCount} kroků)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Odpovědět" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "povinné" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Odpovězeno v chatu" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Zpráva s odpovědí již není zobrazena" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Zkusit odeslat znovu" msgid "Rules" msgstr "Pravidla" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Spustit" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Bezpečné" @@ -2121,6 +2329,10 @@ msgstr "Uloženo" msgid "Saving..." msgstr "Ukládám..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Přejít v chatu na tuto odpověď" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Přejít dolů" msgid "Search" msgstr "Hledat" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Hledat boty" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Hledejte GIFy pro začátek" @@ -2183,6 +2399,10 @@ msgstr "Bezpečnostní upozornění" msgid "Seek" msgstr "Přetočit" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Vyberte bota vlevo, abyste zobrazili jeho příkazy a možnosti správy." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Vybrat kanál" @@ -2195,6 +2415,10 @@ msgstr "Vybrat člena" msgid "Select or add a channel to get started." msgstr "Vyberte nebo přidejte kanál pro začátek." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "samoregistrovaný" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Odeslat GIF" msgid "Send a warning message to {username}" msgstr "Odeslat varovnou zprávu uživateli {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Odeslat akci / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Odeslat pozvánku" @@ -2280,6 +2508,10 @@ msgstr "Heslo serveru" msgid "Server-to-server communication may use unencrypted connections" msgstr "Komunikace mezi servery může používat nešifrovaná připojení" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "Pro celý server" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Nastavit téma…" @@ -2376,6 +2608,10 @@ msgstr "Zobrazuje média z důvěryhodného file hostu vašeho serveru. Nejsou p msgid "Signed On" msgstr "Přihlášen" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Lomítkové příkazy" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Software:" @@ -2392,10 +2628,18 @@ msgstr "Seřadit podle uživatelů" msgid "SoundCloud player" msgstr "Přehrávač SoundCloud" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Zdroj" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Začít soukromou zprávu" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Spouštění pracovního postupu…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Stavové zprávy" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Zastavit" @@ -2419,10 +2664,18 @@ msgstr "Přísný" msgid "Strict - More aggressive protection" msgstr "Přísný - agresivnější ochrana" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Pozastavit" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Systém" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Text" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Textové kanály" @@ -2447,6 +2700,14 @@ msgstr "Téma, které bude zobrazeno pro tento kanál. Téma mohou vidět všich msgid "The user will receive an invitation to join {channelName}." msgstr "Uživatel obdrží pozvánku k připojení do {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Pracovní postup, který vytvořil tuto zprávu, již není ve stavu" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Tento příkaz nemá žádné parametry." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Toto pole je povinné" @@ -2510,6 +2771,10 @@ msgstr "Přepnout oznámení" msgid "Toggle search" msgstr "Přepnout hledání" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Nástroj" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Téma nastaveno po (min)" @@ -2527,6 +2792,10 @@ msgstr "Téma:" msgid "Total: {0}" msgstr "Celkem: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Přenos" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Důvěryhodné zdroje" @@ -2561,6 +2830,10 @@ msgstr "Odepnout soukromou konverzaci" msgid "Unpin this private message conversation" msgstr "Odepnout tuto soukromou konverzaci" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Zrušit pozastavení" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Nahrát" @@ -2687,6 +2960,11 @@ msgstr "Velmi uvolněný" msgid "Very Strict" msgstr "Velmi přísný" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "prostřednictvím @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2730,6 +3008,10 @@ msgstr "Uživatel s hlasem" msgid "Volume" msgstr "Hlasitost" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Čekání na první krok…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Byl vykopnut z kanálu" msgid "We don't store your IRC communications on our servers" msgstr "Neukládáme vaši IRC komunikaci na našich serverech" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Vítejte v {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Co teď děláš?" msgid "What this means:" msgstr "Co to znamená:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Co děláte" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "Na co myslíte?" @@ -2780,6 +3070,10 @@ msgstr "Na co myslíte?" msgid "WHISPER" msgstr "ŠEPOT" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Šeptat uživateli v kontextu aktuálního kanálu" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Široký - širší rozsah ochrany" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Historie pracovních postupů" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Historie pracovních postupů ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Pracuji…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/de/messages.mjs b/src/locales/de/messages.mjs index 0e7ec21c..223ec86e 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: 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\"],\"\"],\"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! 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: 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! Ö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\"],\" 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/invitation-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\"],\"+fRR7i\":[\"Sperren\"],\"+mVPqU\":[\"Markdown-Formatierung in Nachrichten rendern\"],\"+vqCJH\":[\"Ihr Kontobenutzername zur Authentifizierung\"],\"+yPBXI\":[\"Datei auswählen\"],\"+zy2Nq\":[\"Typ\"],\"/09cao\":[\"Geringe Verbindungssicherheit (Stufe \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"Sich als zurück markieren\"],\"/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\"],\"/xQ19T\":[\"Bots in diesem Netzwerk\"],\"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\"],\"1C/fOn\":[\"Der Bot hat noch keine Slash-Befehle registriert.\"],\"1DSr1i\":[\"Konto registrieren\"],\"1O/24y\":[\"Kanalliste umschalten\"],\"1QfxQT\":[\"Ausblenden\"],\"1VPJJ2\":[\"Warnung: Externer Link\"],\"1ZC/dv\":[\"Keine ungelesenen Erwähnungen oder Nachrichten\"],\"1pO1zi\":[\"Servername ist erforderlich\"],\"1t/NnN\":[\"Ablehnen\"],\"1uwfzQ\":[\"Kanalthema anzeigen\"],\"268g7c\":[\"Anzeigenamen eingeben\"],\"2F9+AZ\":[\"Noch kein roher IRC-Datenverkehr aufgezeichnet. 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\"],\"2xXP/g\":[\"Einem Kanal beitreten\"],\"2ygf/L\":[\"← Zurück\"],\"2zEgxj\":[\"GIFs suchen...\"],\"3JjdaA\":[\"Ausführen\"],\"3NJ4MW\":[\"Den Workflow erneut öffnen, der diese Nachricht erzeugt hat (\",[\"stepCount\"],\" Schritte)\"],\"3RdPhl\":[\"Kanal umbenennen\"],\"3THokf\":[\"Benutzer mit Sprachrecht\"],\"3TSz9S\":[\"Minimieren\"],\"3et0TM\":[\"Chat zu dieser Antwort scrollen\"],\"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\"],\"4uKgKr\":[\"AUS\"],\"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 zum Hochladen hier ablegen\"],\"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:\"],\"9q17ZR\":[[\"0\"],\" ist erforderlich.\"],\"9qIYMn\":[\"Neuer Spitzname\"],\"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?\"],\"BIDT9R\":[\"Bots\"],\"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\"],\"CaQ1Gb\":[\"Konfigurationsdefinierter Bot. Bearbeite obbyircd.conf und /REHASH, um den Zustand zu ändern.\"],\"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\"],\"DSHF2K\":[\"Der Workflow, der diese Nachricht erzeugt hat, ist nicht mehr im Zustand vorhanden\"],\"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...\"],\"Eu7YKa\":[\"selbst registriert\"],\"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: 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...\"],\"IcHxhR\":[\"offline\"],\"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\"],\"JMXMCX\":[\"Abwesenheitsnachricht\"],\"JXGkhG\":[\"Kanalnamen ändern (nur Operatoren)\"],\"JYiL1b\":[\"eines von:\"],\"JcD7qf\":[\"Weitere Aktionen\"],\"JdkA+c\":[\"Geheim (+s)\"],\"Jmu12l\":[\"Serverkanäle\"],\"JvQ++s\":[\"Markdown aktivieren\"],\"K2jwh/\":[\"Keine WHOIS-Daten verfügbar\"],\"K4vEhk\":[\"(gesperrt)\"],\"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\"],\"KtADxr\":[\"führte aus\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" Schritt(e) warten auf Genehmigung\"],\"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\\\")\"],\"LYzbQ2\":[\"Werkzeug\"],\"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\"],\"M45wtf\":[\"Dieser Befehl nimmt keine Parameter.\"],\"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\"],\"N40H+G\":[\"Alle\"],\"N7TQbE\":[\"Benutzer zu \",[\"channelName\"],\" einladen\"],\"NCca/o\":[\"Standard-Spitznamen eingeben...\"],\"NQN2HS\":[\"Entsperren\"],\"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+HhhG\":[\"Einem Benutzer im aktuellen Kanalkontext zuflüstern\"],\"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\"]],\"RlCInP\":[\"Slash-Befehle\"],\"RySp6q\":[\"Kommentare ausblenden\"],\"RzfkXn\":[\"Deinen Spitznamen auf diesem Server ändern\"],\"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\"],\"TImSWn\":[\"(verarbeitet von ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Sich als abwesend markieren\"],\"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!\"],\"UqtiKk\":[\"Automatisch ausblenden in \",[\"secondsLeft\"],\"s\"],\"UrEy4W\":[\"Eine private Nachricht an einen Benutzer öffnen\"],\"UtUIRh\":[[\"0\"],\" ältere Nachrichten\"],\"UwzP+U\":[\"Sichere Verbindung\"],\"V0/A4O\":[\"Kanalbesitzer\"],\"V2dwib\":[[\"0\"],\" muss eine Zahl sein.\"],\"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\"],\"XklovM\":[\"Wird ausgeführt…\"],\"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+tK3n\":[\"Erste zu sendende Nachricht\"],\"Y/qryO\":[\"Keine Benutzer gefunden, die deiner Suche entsprechen\"],\"YAqRpI\":[\"Kontoregistrierung für \",[\"account\"],\" erfolgreich: \",[\"message\"]],\"YBXJ7j\":[\"EIN\"],\"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:\"],\"Z7ZXbT\":[\"Genehmigen\"],\"ZJSWfw\":[\"Nachricht beim Trennen vom Server\"],\"ZR1dJ4\":[\"Einladungen\"],\"ZdWg0V\":[\"Im Browser öffnen\"],\"ZhRBbl\":[\"Nachrichten suchen…\"],\"Zmcu3y\":[\"Erweiterte Filter\"],\"ZqLD8l\":[\"Serverweit\"],\"a2/8e5\":[\"Thema gesetzt nach (Min.)\"],\"aHKcKc\":[\"Vorherige Seite\"],\"aJTbXX\":[\"Oper Password\"],\"aP9gNu\":[\"Ausgabe gekürzt\"],\"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\"],\"bFDO8z\":[\"Gateway online\"],\"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.\"],\"bVBC/W\":[\"Gateway verbunden\"],\"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\"],\"cde3ce\":[\"Nachricht an <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\"],\"dRqrdL\":[[\"0\"],\" muss eine ganze Zahl sein.\"],\"dXqxlh\":[\"<0>⚠️ Sicherheitsrisiko! 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\"],\"dnUmOX\":[\"Noch keine Bots in diesem Netzwerk registriert.\"],\"dpCzmC\":[\"Flood-Schutz-Einstellungen\"],\"e7KzRG\":[[\"0\"],\" Schritt(e)\"],\"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: 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.\"],\"fYdEvu\":[\"Workflow-Verlauf (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Wähle links einen Bot aus, um seine Befehle und Verwaltungsaktionen zu sehen.\"],\"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\"],\"jUXib7\":[\"Die Antwortnachricht ist nicht mehr sichtbar\"],\"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:\"],\"kG2fiE\":[\"konfigurationsdefiniert\"],\"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.\"],\"ky2mw7\":[\"über @\",[\"0\"]],\"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\"],\"lH+ed1\":[\"Warten auf den ersten Schritt…\"],\"lHy8N5\":[\"Weitere Kanäle werden geladen...\"],\"lasgrr\":[\"verwendet\"],\"lbpf14\":[[\"value\"],\" beitreten\"],\"lf3MT4\":[\"Zu verlassender Kanal (Standard: aktueller)\"],\"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)\"],\"mDkV0w\":[\"Workflow wird gestartet…\"],\"mEPxTp\":[\"<0>⚠️ Vorsicht! Ö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\"]],\"mHfd/S\":[\"Was du gerade tust\"],\"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\"],\"nX4XLG\":[\"Operator-Aktionen\"],\"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\"],\"oaTtrx\":[\"Bots suchen\"],\"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\"],\"pQBYsE\":[\"Im Chat geantwortet\"],\"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\"],\"qXgujk\":[\"Eine Aktion / Emote senden\"],\"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\"],\" hat folgende Sicherheitsbedenken:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"ist jetzt bekannt als **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" hat dich eingeladen, \",[\"channel\"],\" beizutreten\"],\"sW5OjU\":[\"erforderlich\"],\"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\"],\"thC9Rq\":[\"Einen Kanal verlassen\"],\"tiBsJk\":[\"hat \",[\"channelName\"],\" verlassen\"],\"tt4/UD\":[\"hat sich abgemeldet (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Beizutretender Kanal (#name)\"],\"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\"]],\"uMIUx8\":[\"Bot \",[\"0\"],\" löschen? Dies löscht die Datenbankzeile vorläufig; der Nick kann später erst nach einem /REHASH wiederverwendet werden.\"],\"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\"],\"wCKe3+\":[\"Workflow-Verlauf\"],\"wFjjxZ\":[\"wurde von \",[\"username\"],\" aus \",[\"channelName\"],\" gekickt (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Keine Bann-Ausnahmen gefunden\"],\"wPrGnM\":[\"Kanal-Administrator\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Überlegung\"],\"wbm86v\":[\"Anzeigen, wenn Benutzer Kanäle betreten oder verlassen\"],\"wdxz7K\":[\"Quelle\"],\"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/invitation-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\"],\"xeiujy\":[\"Text\"],\"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\"]],\"zPBDzU\":[\"Workflow abbrechen\"],\"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..6cb877eb 100644 --- a/src/locales/de/messages.po +++ b/src/locales/de/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - IRC in die Zukunft bringen" msgid "— open in viewer" msgstr "— im Viewer öffnen" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(verarbeitet von ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(gesperrt)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {1 weiteres Element anzeigen} other {{1} weitere Element msgid "{0} and {1} are typing..." msgstr "{0} und {1} tippen..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} ist erforderlich." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} tippt..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} muss eine Zahl sein." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} muss eine ganze Zahl sein." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} ältere Nachrichten" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} Schritt(e)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} Schritt(e) warten auf Genehmigung" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Erweiterte Filter" msgid "Album" msgstr "Album" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Alle" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Alle Inhalte" @@ -297,6 +334,12 @@ msgstr "Filter anwenden & Aktualisieren" msgid "Applying..." msgstr "Wird angewendet..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Genehmigen" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Authentifiziertes Konto" msgid "Auto Fallback to Single Line" msgstr "Automatisch auf einzeilig wechseln" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Automatisch ausblenden in {secondsLeft}s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Beim Verbinden automatisch als Operator authentifizieren" @@ -359,6 +406,10 @@ msgstr "Abwesend" msgid "Away from keyboard" msgstr "Nicht am Rechner" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Abwesenheitsnachricht" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Abwesenheitsnachricht" msgid "Away:" msgstr "Abwesend:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Sperren" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Der Bot hat noch keine Slash-Befehle registriert." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Bots" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Bots in diesem Netzwerk" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Alle Kanäle auf dem Server durchsuchen" @@ -430,6 +499,7 @@ msgstr "Alle Kanäle auf dem Server durchsuchen" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Verbindung abbrechen" msgid "Cancel reply" msgstr "Antwort abbrechen" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Workflow abbrechen" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Kanalnamen ändern (nur Operatoren)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Deinen Spitznamen auf diesem Server ändern" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Kanalmodi geändert" @@ -459,6 +537,7 @@ msgstr "Nickname geändert" msgid "changed the topic to: {topic}" msgstr "hat das Thema geändert zu: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Kanal" @@ -519,6 +598,14 @@ msgstr "Kanalbesitzer" msgid "Channel Settings" msgstr "Kanaleinstellungen" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Beizutretender Kanal (#name)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Zu verlassender Kanal (Standard: aktueller)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Kanalthema" @@ -531,6 +618,7 @@ msgstr "Kanal erscheint nicht in LIST- oder NAMES-Befehlen" msgid "channel-name" msgstr "Kanalname" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Kanäle" @@ -606,6 +694,9 @@ msgstr "Client-Limit (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Kommentare" msgid "Comments ({commentCount})" msgstr "Kommentare ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "konfigurationsdefiniert" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Konfigurationsdefinierter Bot. Bearbeite obbyircd.conf und /REHASH, um den Zustand zu ändern." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "Detaillierte Flood-Schutzregeln konfigurieren. Jede Regel legt fest, welche Aktivitäten überwacht werden sollen und welche Maßnahmen bei Überschreitung der Schwellenwerte ergriffen werden." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Standard-Nickname" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Löschen" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Bot {0} löschen? Dies löscht die Datenbankzeile vorläufig; der Nick kann später erst nach einem /REHASH wiederverwendet werden." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Kanal löschen" @@ -843,6 +948,10 @@ msgstr "Entdecken" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Entdecke die Welt von IRC mit ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Ausblenden" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Benachrichtigung schließen" @@ -891,7 +1000,7 @@ msgstr "Herunterladen" #: src/components/layout/ChatArea.tsx msgid "Drop files to upload" -msgstr "Dateien hier ablegen zum Hochladen" +msgstr "Dateien zum Hochladen hier ablegen" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Kanäle filtern..." msgid "Filter members…" msgstr "Mitglieder filtern…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Erste zu sendende Nachricht" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Flood-Profil (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (globaler Ban)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway verbunden" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "Gateway online" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Allgemein" @@ -1186,6 +1307,10 @@ msgstr "Bild {0} von {1}" msgid "Image preview" msgstr "Bildvorschau" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "EIN" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1270,6 +1395,10 @@ msgstr "{0} beitreten" msgid "Join {value}" msgstr "{value} beitreten" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Einem Kanal beitreten" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Mehr über benutzerdefinierte Regeln erfahren →" msgid "Learn more about profiles →" msgstr "Mehr über Profile erfahren →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Einen Kanal verlassen" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Kanal verlassen" @@ -1411,6 +1544,14 @@ msgstr "Profilinformationen und Metadaten verwalten" msgid "Mark as bot - usually 'on' or empty" msgstr "Als Bot markieren – normalerweise 'on' oder leer" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Sich als abwesend markieren" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Sich als zurück markieren" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Max. Benutzer" @@ -1573,6 +1714,10 @@ msgstr "Netzwerkname" msgid "New DM" msgstr "Neue Direktnachricht" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Neuer Spitzname" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Nächstes Bild" @@ -1617,6 +1762,10 @@ msgstr "Keine Bann-Ausnahmen gefunden" msgid "No bans found" msgstr "Keine Sperren gefunden" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Noch keine Bots in diesem Netzwerk registriert." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Kein zentraler Server:" @@ -1672,7 +1821,7 @@ msgstr "Es werden keine Medienvorschauen geladen." #: src/components/ui/RawLogViewer.tsx msgid "No raw IRC traffic captured yet. Try connecting or sending a message." -msgstr "Noch kein roher IRC-Verkehr erfasst. Versuche dich zu verbinden oder eine Nachricht zu senden." +msgstr "Noch kein roher IRC-Datenverkehr aufgezeichnet. Versuche dich zu verbinden oder eine Nachricht zu senden." #: src/components/ui/QuickActions.tsx msgid "No results found" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - IRC in die Zukunft bringen" msgid "Off" msgstr "Aus" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "offline" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Offline" @@ -1754,6 +1908,10 @@ msgstr "Offline" msgid "on" msgstr "an" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "eines von:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Ups! Netz-Split! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Eine private Nachricht an einen Benutzer öffnen" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Kanaleinstellungen öffnen" @@ -1828,10 +1990,23 @@ msgstr "Oper Password" msgid "Oper Username" msgstr "Oper-Benutzername" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Operator-Aktionen" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Optionale Absturzberichte zur App-Verbesserung" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "AUS" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "Ausgabe gekürzt" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Eigentümer" @@ -1994,6 +2169,10 @@ msgstr "Abgangsnachricht" msgid "Quit the server" msgstr "Den Server verlassen" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "führte aus" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Reagieren" @@ -2024,6 +2203,10 @@ msgstr "Grund" msgid "Reason (optional)" msgstr "Grund (optional)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Überlegung" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Erneut mit Server verbinden" @@ -2037,6 +2220,11 @@ msgstr "Aktualisieren" msgid "Register for an account" msgstr "Konto registrieren" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Ablehnen" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Locker" @@ -2077,11 +2265,27 @@ msgstr "Diesen Kanal auf dem Server umbenennen. Alle Benutzer sehen den neuen Na msgid "Render markdown formatting in messages" msgstr "Markdown-Formatierung in Nachrichten rendern" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Den Workflow erneut öffnen, der diese Nachricht erzeugt hat ({stepCount} Schritte)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Antworten" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "erforderlich" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Im Chat geantwortet" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Die Antwortnachricht ist nicht mehr sichtbar" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Erneut senden" msgid "Rules" msgstr "Regeln" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Ausführen" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Sicher" @@ -2121,6 +2329,10 @@ msgstr "Gespeichert" msgid "Saving..." msgstr "Wird gespeichert..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Chat zu dieser Antwort scrollen" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Nach unten scrollen" msgid "Search" msgstr "Suchen" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Bots suchen" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "GIFs suchen, um zu beginnen" @@ -2183,6 +2399,10 @@ msgstr "Sicherheitswarnung" msgid "Seek" msgstr "Vor-/Zurückspulen" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Wähle links einen Bot aus, um seine Befehle und Verwaltungsaktionen zu sehen." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Kanal auswählen" @@ -2195,6 +2415,10 @@ msgstr "Mitglied auswählen" msgid "Select or add a channel to get started." msgstr "Kanal auswählen oder hinzufügen, um zu beginnen." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "selbst registriert" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "GIF senden" msgid "Send a warning message to {username}" msgstr "Warnmeldung an {username} senden" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Eine Aktion / Emote senden" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Einladung senden" @@ -2280,6 +2508,10 @@ msgstr "Server-Passwort" msgid "Server-to-server communication may use unencrypted connections" msgstr "Die Server-zu-Server-Kommunikation verwendet möglicherweise unverschlüsselte Verbindungen" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "Serverweit" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Thema setzen…" @@ -2376,6 +2608,10 @@ msgstr "Zeigt Medien vom vertrauenswürdigen Datei-Host deines Servers. Es werde msgid "Signed On" msgstr "Angemeldet seit" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Slash-Befehle" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Software:" @@ -2392,10 +2628,18 @@ msgstr "Nach Benutzern sortieren" msgid "SoundCloud player" msgstr "SoundCloud-Player" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Quelle" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Privatnachricht starten" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Workflow wird gestartet…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Statusnachrichten" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Stopp" @@ -2419,10 +2664,18 @@ msgstr "Streng" msgid "Strict - More aggressive protection" msgstr "Streng – Aggressiverer Schutz" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Sperren" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "System" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Text" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Textkanäle" @@ -2447,6 +2700,14 @@ msgstr "Das Thema für diesen Kanal. Alle Benutzer können es sehen." msgid "The user will receive an invitation to join {channelName}." msgstr "Der Benutzer erhält eine Einladung, {channelName} beizutreten." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Der Workflow, der diese Nachricht erzeugt hat, ist nicht mehr im Zustand vorhanden" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Dieser Befehl nimmt keine Parameter." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Dieses Feld ist erforderlich" @@ -2510,6 +2771,10 @@ msgstr "Benachrichtigungen umschalten" msgid "Toggle search" msgstr "Suche umschalten" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Werkzeug" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Thema gesetzt nach (Min.)" @@ -2527,6 +2792,10 @@ msgstr "Thema:" msgid "Total: {0}" msgstr "Gesamt: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Transport" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Vertrauenswürdige Quellen" @@ -2561,6 +2830,10 @@ msgstr "Privatnachricht loslösen" msgid "Unpin this private message conversation" msgstr "Dieses Privatgespräch loslösen" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Entsperren" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Hochladen" @@ -2687,6 +2960,11 @@ msgstr "Sehr locker" msgid "Very Strict" msgstr "Sehr streng" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "über @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2730,6 +3008,10 @@ msgstr "Benutzer mit Sprachrecht" msgid "Volume" msgstr "Lautstärke" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Warten auf den ersten Schritt…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Wurde aus dem Kanal geworfen" msgid "We don't store your IRC communications on our servers" msgstr "Wir speichern Ihre IRC-Kommunikation nicht auf unseren Servern" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Willkommen bei {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Was machst du gerade?" msgid "What this means:" msgstr "Was das bedeutet:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Was du gerade tust" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "Was denkst du gerade?" @@ -2780,6 +3070,10 @@ msgstr "Was denkst du gerade?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Einem Benutzer im aktuellen Kanalkontext zuflüstern" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Weit – Breiterer Schutzbereich" @@ -2788,6 +3082,19 @@ msgstr "Weit – Breiterer Schutzbereich" msgid "Will default to 'no reason' if left empty" msgstr "Standardmäßig 'kein Grund', wenn leer gelassen" +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Workflow-Verlauf" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Workflow-Verlauf ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Wird ausgeführt…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/en/messages.mjs b/src/locales/en/messages.mjs index 1387cd2d..5ef95118 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: 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\"],\"\"],\"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! 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: 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! 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\"],\" 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/invitationcapability 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\"],\"+fRR7i\":[\"Suspend\"],\"+mVPqU\":[\"Render markdown formatting in messages\"],\"+vqCJH\":[\"Your account username for authentication\"],\"+yPBXI\":[\"Choose file\"],\"+zy2Nq\":[\"Type\"],\"/09cao\":[\"Low Link Security (Level \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"Mark yourself as back\"],\"/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\"],\"/xQ19T\":[\"Bots on this network\"],\"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\"],\"1C/fOn\":[\"Bot hasn't registered any slash commands yet.\"],\"1DSr1i\":[\"Register for an account\"],\"1O/24y\":[\"Toggle Channel List\"],\"1QfxQT\":[\"Dismiss\"],\"1VPJJ2\":[\"External Link Warning\"],\"1ZC/dv\":[\"No unread mentions or messages\"],\"1pO1zi\":[\"Server name is required\"],\"1t/NnN\":[\"Reject\"],\"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\"],\"2xXP/g\":[\"Join a channel\"],\"2ygf/L\":[\"← Back\"],\"2zEgxj\":[\"Search GIFs...\"],\"3JjdaA\":[\"Run\"],\"3NJ4MW\":[\"Reopen the workflow that produced this message (\",[\"stepCount\"],\" steps)\"],\"3RdPhl\":[\"Rename Channel\"],\"3THokf\":[\"Voiced User\"],\"3TSz9S\":[\"Minimize\"],\"3et0TM\":[\"Scroll chat to this response\"],\"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\"],\"4uKgKr\":[\"OUT\"],\"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:\"],\"9q17ZR\":[[\"0\"],\" is required.\"],\"9qIYMn\":[\"New nickname\"],\"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?\"],\"BIDT9R\":[\"Bots\"],\"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\"],\"CaQ1Gb\":[\"Config-defined bot. Edit obbyircd.conf and /REHASH to change state.\"],\"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\"],\"DSHF2K\":[\"The workflow that produced this message is no longer in state\"],\"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...\"],\"Eu7YKa\":[\"self-registered\"],\"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: 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...\"],\"IcHxhR\":[\"offline\"],\"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\"],\"JMXMCX\":[\"Away message\"],\"JXGkhG\":[\"Change the channel name (operators only)\"],\"JYiL1b\":[\"one of:\"],\"JcD7qf\":[\"More actions\"],\"JdkA+c\":[\"Secret (+s)\"],\"Jmu12l\":[\"Server Channels\"],\"JvQ++s\":[\"Enable Markdown\"],\"K2jwh/\":[\"No WHOIS data available\"],\"K4vEhk\":[\"(suspended)\"],\"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\"],\"KtADxr\":[\"ran\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" step(s) awaiting approval\"],\"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\\\")\"],\"LYzbQ2\":[\"Tool\"],\"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\"],\"M45wtf\":[\"This command takes no parameters.\"],\"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\"],\"N40H+G\":[\"All\"],\"N7TQbE\":[\"Invite User to \",[\"channelName\"]],\"NCca/o\":[\"Enter default nickname...\"],\"NQN2HS\":[\"Unsuspend\"],\"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+HhhG\":[\"Whisper to a user in the current channel context\"],\"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\"]],\"RlCInP\":[\"Slash commands\"],\"RySp6q\":[\"Hide comments\"],\"RzfkXn\":[\"Change your nickname on this server\"],\"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\"],\"TImSWn\":[\"(handled by ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Mark yourself as away\"],\"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!\"],\"UqtiKk\":[\"Auto-dismiss in \",[\"secondsLeft\"],\"s\"],\"UrEy4W\":[\"Open a private message to a user\"],\"UtUIRh\":[[\"0\"],\" older messages\"],\"UwzP+U\":[\"Secure Connection\"],\"V0/A4O\":[\"Channel Owner\"],\"V2dwib\":[[\"0\"],\" must be a number.\"],\"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\"],\"XklovM\":[\"Working…\"],\"Xm/s+u\":[\"Display\"],\"Xp2n93\":[\"Shows media from your server's trusted file host. No requests are made to external services.\"],\"XvjC4F\":[\"Saving...\"],\"Y+tK3n\":[\"First message to send\"],\"Y/qryO\":[\"No users found matching your search\"],\"YAqRpI\":[\"Account registration successful for \",[\"account\"],\": \",[\"message\"]],\"YBXJ7j\":[\"IN\"],\"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:\"],\"Z7ZXbT\":[\"Approve\"],\"ZJSWfw\":[\"Message shown when you disconnect from the server\"],\"ZR1dJ4\":[\"Invitations\"],\"ZdWg0V\":[\"Open in browser\"],\"ZhRBbl\":[\"Search messages…\"],\"Zmcu3y\":[\"Advanced Filters\"],\"ZqLD8l\":[\"Server-wide\"],\"a2/8e5\":[\"Topic Set After (min ago)\"],\"aHKcKc\":[\"Previous page\"],\"aJTbXX\":[\"Oper Password\"],\"aP9gNu\":[\"output truncated\"],\"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\"],\"bFDO8z\":[\"gateway online\"],\"bQ6BJn\":[\"Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded.\"],\"bVBC/W\":[\"Gateway connected\"],\"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\"],\"cde3ce\":[\"Message <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\"],\"dRqrdL\":[[\"0\"],\" must be a whole number.\"],\"dXqxlh\":[\"<0>⚠️ Security Risk! This connection may be vulnerable to interception or man-in-the-middle attacks.\"],\"da9Q/R\":[\"Changed channel modes\"],\"dhJN3N\":[\"Show comments\"],\"dj2xTE\":[\"Dismiss notification\"],\"dnUmOX\":[\"No bots registered on this network yet.\"],\"dpCzmC\":[\"Flood Protection Settings\"],\"e7KzRG\":[[\"0\"],\" step(s)\"],\"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: 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.\"],\"fYdEvu\":[\"Workflow history (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Select a bot on the left to see its commands and management actions.\"],\"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\"],\"jUXib7\":[\"Response message is no longer in view\"],\"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:\"],\"kG2fiE\":[\"config-defined\"],\"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.\"],\"ky2mw7\":[\"via @\",[\"0\"]],\"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\"],\"lH+ed1\":[\"Waiting for first step…\"],\"lHy8N5\":[\"Loading more channels...\"],\"lasgrr\":[\"used\"],\"lbpf14\":[\"Join \",[\"value\"]],\"lf3MT4\":[\"Channel to leave (defaults to current)\"],\"lfFsZ4\":[\"Channels\"],\"lkNdiH\":[\"Account Name\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Upload Image\"],\"loQxaJ\":[\"I'm Back\"],\"lvfaxv\":[\"HOME\"],\"m16xKo\":[\"Add\"],\"m8flAk\":[\"Preview (not yet uploaded)\"],\"mDkV0w\":[\"Starting workflow…\"],\"mEPxTp\":[\"<0>⚠️ Be careful! Only open links from trusted sources. Malicious links can compromise your security or privacy.\"],\"mHGdhG\":[\"Server Information\"],\"mHS8lb\":[\"Message #\",[\"0\"]],\"mHfd/S\":[\"What you're doing\"],\"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\"],\"nX4XLG\":[\"Operator actions\"],\"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\"],\"oaTtrx\":[\"Search bots\"],\"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\"],\"pQBYsE\":[\"Responded in chat\"],\"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\"],\"qXgujk\":[\"Send an action / emote\"],\"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\"],\" has the following security concerns:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"is now known as **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" has invited you to join \",[\"channel\"]],\"sW5OjU\":[\"required\"],\"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\"],\"thC9Rq\":[\"Leave a channel\"],\"tiBsJk\":[\"left \",[\"channelName\"]],\"tt4/UD\":[\"quit (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Channel to join (#name)\"],\"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\"]],\"uMIUx8\":[\"Delete bot \",[\"0\"],\"? This soft-deletes the database row; reuse the nick later only after a /REHASH.\"],\"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\"],\"wCKe3+\":[\"Workflow history\"],\"wFjjxZ\":[\"was kicked from \",[\"channelName\"],\" by \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"No ban exceptions found\"],\"wPrGnM\":[\"Channel Admin\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Reasoning\"],\"wbm86v\":[\"Display when users join or leave channels\"],\"wdxz7K\":[\"Source\"],\"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/invitationcapability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks.\"],\"xceQrO\":[\"Only secure websockets are supported\"],\"xdtXa+\":[\"channel-name\"],\"xeiujy\":[\"Text\"],\"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\"]],\"zPBDzU\":[\"Cancel workflow\"],\"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..b7570584 100644 --- a/src/locales/en/messages.po +++ b/src/locales/en/messages.po @@ -21,6 +21,14 @@ msgstr "ObsidianIRC - Bringing IRC to the future" msgid "— open in viewer" msgstr "— open in viewer" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(handled by ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(suspended)" + #. 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); @@ -40,16 +48,41 @@ msgstr "{0, plural, one {Show 1 more item} other {Show {1} more items}}" msgid "{0} and {1} are typing..." msgstr "{0} and {1} are typing..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} is required." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} is typing..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} must be a number." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} must be a whole number." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} older messages" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} step(s)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} step(s) awaiting approval" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -251,6 +284,10 @@ msgstr "Advanced Filters" msgid "Album" msgstr "Album" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "All" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "All Content" @@ -296,6 +333,12 @@ msgstr "Apply Filters & Refresh" msgid "Applying..." msgstr "Applying..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Approve" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -329,6 +372,10 @@ msgstr "Authenticated Account" msgid "Auto Fallback to Single Line" msgstr "Auto Fallback to Single Line" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Auto-dismiss in {secondsLeft}s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Automatically authenticate as operator on connect" @@ -358,6 +405,10 @@ msgstr "Away" msgid "Away from keyboard" msgstr "Away from keyboard" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Away message" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -367,6 +418,7 @@ msgstr "Away Message" msgid "Away:" msgstr "Away:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -412,11 +464,28 @@ msgstr "Bans" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Bot hasn't registered any slash commands yet." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Bots" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Bots on this network" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Browse all channels on the server" @@ -429,6 +498,7 @@ msgstr "Browse all channels on the server" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -442,10 +512,18 @@ msgstr "Cancel Connection" msgid "Cancel reply" msgstr "Cancel reply" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Cancel workflow" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Change the channel name (operators only)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Change your nickname on this server" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Changed channel modes" @@ -458,6 +536,7 @@ msgstr "Changed nickname" msgid "changed the topic to: {topic}" msgstr "changed the topic to: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Channel" @@ -518,6 +597,14 @@ msgstr "Channel Owner" msgid "Channel Settings" msgstr "Channel Settings" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Channel to join (#name)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Channel to leave (defaults to current)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Channel Topic" @@ -530,6 +617,7 @@ msgstr "Channel won't appear in LIST or NAMES commands" msgid "channel-name" msgstr "channel-name" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Channels" @@ -605,6 +693,9 @@ msgstr "Client Limit (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -655,6 +746,14 @@ msgstr "Comments" msgid "Comments ({commentCount})" msgstr "Comments ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "config-defined" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." @@ -801,10 +900,16 @@ msgid "Default Nickname" msgstr "Default Nickname" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Delete" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Delete Channel" @@ -842,6 +947,10 @@ msgstr "Discover" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Discover the world of IRC with ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Dismiss" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Dismiss notification" @@ -1038,6 +1147,10 @@ msgstr "Filter channels..." msgid "Filter members…" msgstr "Filter members…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "First message to send" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Flood Profile (+F)" @@ -1068,6 +1181,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (Global Ban)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway connected" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway online" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "General" @@ -1185,6 +1306,10 @@ msgstr "Image {0} of {1}" msgid "Image preview" msgstr "Image preview" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "IN" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1269,6 +1394,10 @@ msgstr "Join {0}" msgid "Join {value}" msgstr "Join {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Join a channel" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1316,6 +1445,10 @@ msgstr "Learn more about custom rules →" msgid "Learn more about profiles →" msgstr "Learn more about profiles →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Leave a channel" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Leave channel" @@ -1410,6 +1543,14 @@ msgstr "Manage your profile information and metadata" msgid "Mark as bot - usually 'on' or empty" msgstr "Mark as bot - usually 'on' or empty" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Mark yourself as away" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Mark yourself as back" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Max Users" @@ -1572,6 +1713,10 @@ msgstr "Network Name" msgid "New DM" msgstr "New DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "New nickname" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Next image" @@ -1616,6 +1761,10 @@ msgstr "No ban exceptions found" msgid "No bans found" msgstr "No bans found" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "No bots registered on this network yet." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "No Central Server:" @@ -1743,9 +1892,14 @@ msgstr "ObsidianIRC - Bringing IRC to the future" msgid "Off" msgstr "Off" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "offline" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Offline" @@ -1753,6 +1907,10 @@ msgstr "Offline" msgid "on" msgstr "on" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "one of:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1783,6 +1941,10 @@ msgstr "Oops! The net split! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Open a private message to a user" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Open channel configuration settings" @@ -1827,10 +1989,23 @@ msgstr "Oper Password" msgid "Oper Username" msgstr "Oper Username" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Operator actions" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Optional crash reports to improve the app" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "OUT" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "output truncated" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Owner" @@ -1993,6 +2168,10 @@ msgstr "Quit Message" msgid "Quit the server" msgstr "Quit the server" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "ran" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "React" @@ -2023,6 +2202,10 @@ msgstr "Reason" msgid "Reason (optional)" msgstr "Reason (optional)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Reasoning" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Reconnect to server" @@ -2036,6 +2219,11 @@ msgstr "Refresh" msgid "Register for an account" msgstr "Register for an account" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Reject" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Relaxed" @@ -2076,11 +2264,27 @@ msgstr "Rename this channel on the server. All users will see the new name." msgid "Render markdown formatting in messages" msgstr "Render markdown formatting in messages" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Reopen the workflow that produced this message ({stepCount} steps)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Reply" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "required" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Responded in chat" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Response message is no longer in view" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2090,6 +2294,10 @@ msgstr "Retry sending" msgid "Rules" msgstr "Rules" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Run" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Safe" @@ -2120,6 +2328,10 @@ msgstr "Saved" msgid "Saving..." msgstr "Saving..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Scroll chat to this response" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2133,6 +2345,10 @@ msgstr "Scroll to bottom" msgid "Search" msgstr "Search" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Search bots" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Search for GIFs to get started" @@ -2182,6 +2398,10 @@ msgstr "Security Warning" msgid "Seek" msgstr "Seek" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Select a bot on the left to see its commands and management actions." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Select a channel" @@ -2194,6 +2414,10 @@ msgstr "Select Member" msgid "Select or add a channel to get started." msgstr "Select or add a channel to get started." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "self-registered" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2203,6 +2427,10 @@ msgstr "Send a GIF" msgid "Send a warning message to {username}" msgstr "Send a warning message to {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Send an action / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Send Invite" @@ -2279,6 +2507,10 @@ msgstr "Server Password" msgid "Server-to-server communication may use unencrypted connections" msgstr "Server-to-server communication may use unencrypted connections" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "Server-wide" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Set a topic…" @@ -2375,6 +2607,10 @@ msgstr "Shows media from your server's trusted file host. No requests are made t msgid "Signed On" msgstr "Signed On" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Slash commands" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Software:" @@ -2391,10 +2627,18 @@ msgstr "Sort by Users" msgid "SoundCloud player" msgstr "SoundCloud player" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Source" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Start Private Message" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Starting workflow…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2406,6 +2650,7 @@ msgid "Status Messages" msgstr "Status Messages" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Stop" @@ -2418,10 +2663,18 @@ msgstr "Strict" msgid "Strict - More aggressive protection" msgstr "Strict - More aggressive protection" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Suspend" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "System" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Text" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Text Channels" @@ -2446,6 +2699,14 @@ msgstr "The topic that will be displayed for this channel. All users can see the msgid "The user will receive an invitation to join {channelName}." msgstr "The user will receive an invitation to join {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "The workflow that produced this message is no longer in state" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "This command takes no parameters." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "This field is required" @@ -2509,6 +2770,10 @@ msgstr "Toggle Notifications" msgid "Toggle search" msgstr "Toggle search" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Tool" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Topic Set After (min ago)" @@ -2526,6 +2791,10 @@ msgstr "Topic:" msgid "Total: {0}" msgstr "Total: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Transport" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Trusted Sources" @@ -2560,6 +2829,10 @@ msgstr "Unpin Private Chat" msgid "Unpin this private message conversation" msgstr "Unpin this private message conversation" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Unsuspend" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Upload" @@ -2686,6 +2959,11 @@ msgstr "Very Relaxed" msgid "Very Strict" msgstr "Very Strict" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "via @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2729,6 +3007,10 @@ msgstr "Voiced User" msgid "Volume" msgstr "Volume" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Waiting for first step…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2750,6 +3032,10 @@ msgstr "Was kicked from the channel" msgid "We don't store your IRC communications on our servers" msgstr "We don't store your IRC communications on our servers" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2771,6 +3057,10 @@ msgstr "What are you up to?" msgid "What this means:" msgstr "What this means:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "What you're doing" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "What's on your mind?" @@ -2779,6 +3069,10 @@ msgstr "What's on your mind?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Whisper to a user in the current channel context" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Wide - Broader protection scope" @@ -2787,6 +3081,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Workflow history" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Workflow history ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Working…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/es/messages.mjs b/src/locales/es/messages.mjs index b1474fac..ac80eef0 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: 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\"],\"\"],\"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! 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: 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! 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\"],\" 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/invitation). 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\"],\"+fRR7i\":[\"Suspender\"],\"+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\"],\")\"],\"/0mUpR\":[\"Marcarse como de vuelta\"],\"/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\"],\"/xQ19T\":[\"Bots en esta red\"],\"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\"],\"1C/fOn\":[\"El bot aún no ha registrado ningún comando slash.\"],\"1DSr1i\":[\"Registrar una cuenta\"],\"1O/24y\":[\"Alternar lista de canales\"],\"1QfxQT\":[\"Descartar\"],\"1VPJJ2\":[\"Advertencia de enlace externo\"],\"1ZC/dv\":[\"Sin menciones ni mensajes sin leer\"],\"1pO1zi\":[\"El nombre del servidor es obligatorio\"],\"1t/NnN\":[\"Rechazar\"],\"1uwfzQ\":[\"Ver tema del canal\"],\"268g7c\":[\"Ingresa el nombre a mostrar\"],\"2F9+AZ\":[\"Aún no se ha capturado tráfico IRC en bruto. Prueba a conectarte o a 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\"],\"2xXP/g\":[\"Unirse a un canal\"],\"2ygf/L\":[\"← Atrás\"],\"2zEgxj\":[\"Buscar GIFs...\"],\"3JjdaA\":[\"Ejecutar\"],\"3NJ4MW\":[\"Reabrir el flujo de trabajo que produjo este mensaje (\",[\"stepCount\"],\" pasos)\"],\"3RdPhl\":[\"Renombrar canal\"],\"3THokf\":[\"Usuario con voz\"],\"3TSz9S\":[\"Minimizar\"],\"3et0TM\":[\"Desplazar el chat a esta respuesta\"],\"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\"],\"4uKgKr\":[\"SALIDA\"],\"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 los 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:\"],\"9q17ZR\":[[\"0\"],\" es obligatorio.\"],\"9qIYMn\":[\"Nuevo apodo\"],\"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?\"],\"BIDT9R\":[\"Bots\"],\"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\"],\"CaQ1Gb\":[\"Bot definido por configuración. Edita obbyircd.conf y /REHASH para cambiar su estado.\"],\"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\"],\"DSHF2K\":[\"El flujo de trabajo que produjo este mensaje ya no está en el estado\"],\"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...\"],\"Eu7YKa\":[\"autorregistrado\"],\"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: 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...\"],\"IcHxhR\":[\"desconectado\"],\"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\"],\"JMXMCX\":[\"Mensaje de ausencia\"],\"JXGkhG\":[\"Cambiar el nombre del canal (solo operadores)\"],\"JYiL1b\":[\"uno de:\"],\"JcD7qf\":[\"Más acciones\"],\"JdkA+c\":[\"Secreto (+s)\"],\"Jmu12l\":[\"Canales del servidor\"],\"JvQ++s\":[\"Activar Markdown\"],\"K2jwh/\":[\"No hay datos WHOIS disponibles\"],\"K4vEhk\":[\"(suspendido)\"],\"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\"],\"KtADxr\":[\"ejecutó\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" paso(s) en espera de aprobación\"],\"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\\\")\"],\"LYzbQ2\":[\"Herramienta\"],\"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\"],\"M45wtf\":[\"Este comando no toma parámetros.\"],\"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\"],\"N40H+G\":[\"Todos\"],\"N7TQbE\":[\"Invitar usuario a \",[\"channelName\"]],\"NCca/o\":[\"Ingresa apodo predeterminado...\"],\"NQN2HS\":[\"Reactivar\"],\"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+HhhG\":[\"Susurrar a un usuario en el contexto del canal actual\"],\"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\"]],\"RlCInP\":[\"Comandos slash\"],\"RySp6q\":[\"Ocultar comentarios\"],\"RzfkXn\":[\"Cambia tu apodo en este servidor\"],\"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\"],\"TImSWn\":[\"(gestionado por ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Marcarse como ausente\"],\"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!\"],\"UqtiKk\":[\"Descartar automáticamente en \",[\"secondsLeft\"],\"s\"],\"UrEy4W\":[\"Abrir un mensaje privado con un usuario\"],\"UtUIRh\":[[\"0\"],\" mensajes anteriores\"],\"UwzP+U\":[\"Conexión segura\"],\"V0/A4O\":[\"Propietario del canal\"],\"V2dwib\":[[\"0\"],\" debe ser un número.\"],\"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\"],\"XklovM\":[\"Procesando…\"],\"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+tK3n\":[\"Primer mensaje a enviar\"],\"Y/qryO\":[\"No se encontraron usuarios que coincidan con tu búsqueda\"],\"YAqRpI\":[\"Registro de cuenta exitoso para \",[\"account\"],\": \",[\"message\"]],\"YBXJ7j\":[\"ENTRADA\"],\"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:\"],\"Z7ZXbT\":[\"Aprobar\"],\"ZJSWfw\":[\"Mensaje al desconectarse del servidor\"],\"ZR1dJ4\":[\"Invitaciones\"],\"ZdWg0V\":[\"Abrir en el navegador\"],\"ZhRBbl\":[\"Buscar mensajes…\"],\"Zmcu3y\":[\"Filtros avanzados\"],\"ZqLD8l\":[\"A nivel del servidor\"],\"a2/8e5\":[\"Tema establecido hace más de (min)\"],\"aHKcKc\":[\"Página anterior\"],\"aJTbXX\":[\"Contraseña de oper\"],\"aP9gNu\":[\"salida truncada\"],\"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\"],\"bFDO8z\":[\"gateway en línea\"],\"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.\"],\"bVBC/W\":[\"Gateway conectado\"],\"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\"],\"cde3ce\":[\"Mensaje a <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\"],\"dRqrdL\":[[\"0\"],\" debe ser un número entero.\"],\"dXqxlh\":[\"<0>⚠️ ¡Riesgo de seguridad! 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\"],\"dnUmOX\":[\"Aún no hay bots registrados en esta red.\"],\"dpCzmC\":[\"Configuración de protección contra flood\"],\"e7KzRG\":[[\"0\"],\" paso(s)\"],\"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: 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.\"],\"fYdEvu\":[\"Historial de flujos de trabajo (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Selecciona un bot a la izquierda para ver sus comandos y acciones de gestión.\"],\"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\"],\"jUXib7\":[\"El mensaje de respuesta ya no está a la vista\"],\"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:\"],\"kG2fiE\":[\"definido por configuración\"],\"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.\"],\"ky2mw7\":[\"vía @\",[\"0\"]],\"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\"],\"lH+ed1\":[\"Esperando el primer paso…\"],\"lHy8N5\":[\"Cargando más canales...\"],\"lasgrr\":[\"usado\"],\"lbpf14\":[\"Unirse a \",[\"value\"]],\"lf3MT4\":[\"Canal del que salir (por defecto el actual)\"],\"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)\"],\"mDkV0w\":[\"Iniciando flujo de trabajo…\"],\"mEPxTp\":[\"<0>⚠️ ¡Ten cuidado! 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\"]],\"mHfd/S\":[\"Lo que estás haciendo\"],\"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\"],\"nX4XLG\":[\"Acciones de operador\"],\"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\"],\"oaTtrx\":[\"Buscar bots\"],\"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\"],\"pQBYsE\":[\"Respondió en el chat\"],\"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\"],\"qXgujk\":[\"Enviar una acción / emote\"],\"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\"],\" presenta los siguientes problemas de seguridad:\"],\"sGH11W\":[\"Servidor\"],\"sHI1H+\":[\"ahora se llama **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" te ha invitado a unirte a \",[\"channel\"]],\"sW5OjU\":[\"requerido\"],\"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\"],\"thC9Rq\":[\"Salir de un canal\"],\"tiBsJk\":[\"salió de \",[\"channelName\"]],\"tt4/UD\":[\"salió (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Canal al que unirse (#nombre)\"],\"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\"]],\"uMIUx8\":[\"¿Eliminar el bot \",[\"0\"],\"? Esto elimina la fila de la base de datos de forma reversible; reutiliza el nick más tarde solo después de un /REHASH.\"],\"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\"],\"wCKe3+\":[\"Historial de flujos de trabajo\"],\"wFjjxZ\":[\"fue expulsado de \",[\"channelName\"],\" por \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"No se encontraron excepciones de ban\"],\"wPrGnM\":[\"Administrador del canal\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Razonamiento\"],\"wbm86v\":[\"Mostrar cuando los usuarios entran o salen de canales\"],\"wdxz7K\":[\"Origen\"],\"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/invitation). Puedes seguir chateando con normalidad; este panel es para redes basadas en obbyircd.\"],\"xceQrO\":[\"Solo se admiten websockets seguros\"],\"xdtXa+\":[\"nombre-del-canal\"],\"xeiujy\":[\"Texto\"],\"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\"]],\"zPBDzU\":[\"Cancelar flujo de trabajo\"],\"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..531708bc 100644 --- a/src/locales/es/messages.po +++ b/src/locales/es/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Llevando IRC al futuro" msgid "— open in viewer" msgstr "— abrir en visor" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(gestionado por ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(suspendido)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Mostrar 1 elemento más} other {Mostrar {1} elementos m msgid "{0} and {1} are typing..." msgstr "{0} y {1} están escribiendo..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} es obligatorio." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} está escribiendo..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} debe ser un número." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} debe ser un número entero." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} mensajes anteriores" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} paso(s)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} paso(s) en espera de aprobación" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Filtros avanzados" msgid "Album" msgstr "Álbum" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Todos" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Todo el contenido" @@ -297,6 +334,12 @@ msgstr "Aplicar filtros y actualizar" msgid "Applying..." msgstr "Aplicando..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Aprobar" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Cuenta autenticada" msgid "Auto Fallback to Single Line" msgstr "Retorno automático a línea única" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Descartar automáticamente en {secondsLeft}s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Autenticarse automáticamente como operador al conectar" @@ -359,6 +406,10 @@ msgstr "Ausente" msgid "Away from keyboard" msgstr "Alejado del teclado" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Mensaje de ausencia" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Mensaje de ausencia" msgid "Away:" msgstr "Ausente:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Bans" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "El bot aún no ha registrado ningún comando slash." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Bots" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Bots en esta red" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Ver todos los canales del servidor" @@ -430,6 +499,7 @@ msgstr "Ver todos los canales del servidor" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Cancelar conexión" msgid "Cancel reply" msgstr "Cancelar respuesta" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Cancelar flujo de trabajo" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Cambiar el nombre del canal (solo operadores)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Cambia tu apodo en este servidor" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Modos del canal cambiados" @@ -459,6 +537,7 @@ msgstr "Apodo cambiado" msgid "changed the topic to: {topic}" msgstr "cambió el tema a: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Canal" @@ -519,6 +598,14 @@ msgstr "Propietario del canal" msgid "Channel Settings" msgstr "Configuración del canal" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Canal al que unirse (#nombre)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Canal del que salir (por defecto el actual)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Tema del canal" @@ -531,6 +618,7 @@ msgstr "El canal no aparecerá en los comandos LIST ni NAMES" msgid "channel-name" msgstr "nombre-del-canal" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Canales" @@ -606,6 +694,9 @@ msgstr "Límite de usuarios (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Comentarios" msgid "Comments ({commentCount})" msgstr "Comentarios ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "definido por configuración" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Bot definido por configuración. Edita obbyircd.conf y /REHASH para cambiar su estado." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "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." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Apodo predeterminado" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Eliminar" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "¿Eliminar el bot {0}? Esto elimina la fila de la base de datos de forma reversible; reutiliza el nick más tarde solo después de un /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Eliminar canal" @@ -843,6 +948,10 @@ msgstr "Explorar" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Descubre el mundo de IRC con ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Descartar" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Descartar notificación" @@ -891,7 +1000,7 @@ msgstr "Descargar" #: src/components/layout/ChatArea.tsx msgid "Drop files to upload" -msgstr "Suelta archivos para subirlos" +msgstr "Suelta los archivos para subirlos" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Filtrar canales..." msgid "Filter members…" msgstr "Filtrar miembros…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Primer mensaje a enviar" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Perfil de flood (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (ban global)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway conectado" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway en línea" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "General" @@ -1186,6 +1307,10 @@ msgstr "Imagen {0} de {1}" msgid "Image preview" msgstr "Vista previa de imagen" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "ENTRADA" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1270,6 +1395,10 @@ msgstr "Unirse a {0}" msgid "Join {value}" msgstr "Unirse a {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Unirse a un canal" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Más información sobre reglas personalizadas →" msgid "Learn more about profiles →" msgstr "Más información sobre perfiles →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Salir de un canal" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Salir del canal" @@ -1411,6 +1544,14 @@ msgstr "Administra la información y metadatos de tu perfil" msgid "Mark as bot - usually 'on' or empty" msgstr "Marcar como bot, normalmente 'on' o vacío" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Marcarse como ausente" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Marcarse como de vuelta" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Máximo de usuarios" @@ -1573,6 +1714,10 @@ msgstr "Nombre de red" msgid "New DM" msgstr "Nuevo mensaje directo" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Nuevo apodo" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Imagen siguiente" @@ -1617,6 +1762,10 @@ msgstr "No se encontraron excepciones de ban" msgid "No bans found" msgstr "No se encontraron bans" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Aún no hay bots registrados en esta red." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Sin servidor central:" @@ -1672,7 +1821,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 en bruto. Prueba a conectarte o a enviar un mensaje." #: src/components/ui/QuickActions.tsx msgid "No results found" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - Llevando IRC al futuro" msgid "Off" msgstr "Desactivado" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "desconectado" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Desconectado" @@ -1754,6 +1908,10 @@ msgstr "Desconectado" msgid "on" msgstr "activado" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "uno de:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "¡Vaya! ¡División de red! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Abrir un mensaje privado con un usuario" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Abrir configuración del canal" @@ -1828,10 +1990,23 @@ msgstr "Contraseña de oper" msgid "Oper Username" msgstr "Nombre de usuario Oper" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Acciones de operador" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Informes de errores opcionales para mejorar la app" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "SALIDA" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "salida truncada" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Propietario" @@ -1994,6 +2169,10 @@ msgstr "Mensaje de salida" msgid "Quit the server" msgstr "Salió del servidor" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "ejecutó" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Reaccionar" @@ -2024,6 +2203,10 @@ msgstr "Motivo" msgid "Reason (optional)" msgstr "Motivo (opcional)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Razonamiento" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Reconectar al servidor" @@ -2037,6 +2220,11 @@ msgstr "Actualizar" msgid "Register for an account" msgstr "Registrar una cuenta" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Rechazar" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Relajado" @@ -2077,11 +2265,27 @@ msgstr "Renombrar este canal en el servidor. Todos los usuarios verán el nuevo msgid "Render markdown formatting in messages" msgstr "Renderizar formato Markdown en mensajes" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Reabrir el flujo de trabajo que produjo este mensaje ({stepCount} pasos)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Responder" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "requerido" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Respondió en el chat" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "El mensaje de respuesta ya no está a la vista" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Reintentar envío" msgid "Rules" msgstr "Reglas" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Ejecutar" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Seguro" @@ -2121,6 +2329,10 @@ msgstr "Guardado" msgid "Saving..." msgstr "Guardando..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Desplazar el chat a esta respuesta" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Ir al final" msgid "Search" msgstr "Buscar" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Buscar bots" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Busca GIFs para empezar" @@ -2183,6 +2399,10 @@ msgstr "Advertencia de seguridad" msgid "Seek" msgstr "Buscar posición" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Selecciona un bot a la izquierda para ver sus comandos y acciones de gestión." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Selecciona un canal" @@ -2195,6 +2415,10 @@ msgstr "Seleccionar miembro" msgid "Select or add a channel to get started." msgstr "Selecciona o agrega un canal para comenzar." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "autorregistrado" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Enviar un GIF" msgid "Send a warning message to {username}" msgstr "Enviar un mensaje de advertencia a {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Enviar una acción / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Enviar invitación" @@ -2280,6 +2508,10 @@ msgstr "Contraseña del servidor" msgid "Server-to-server communication may use unencrypted connections" msgstr "La comunicación entre servidores puede usar conexiones sin cifrar" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "A nivel del servidor" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Establece un tema…" @@ -2376,6 +2608,10 @@ msgstr "Muestra medios desde el host de archivos de confianza de tu servidor. No msgid "Signed On" msgstr "Conectado desde" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Comandos slash" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Software:" @@ -2392,10 +2628,18 @@ msgstr "Ordenar por usuarios" msgid "SoundCloud player" msgstr "Reproductor de SoundCloud" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Origen" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Iniciar mensaje privado" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Iniciando flujo de trabajo…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Mensajes de estado" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Detener" @@ -2419,10 +2664,18 @@ msgstr "Estricto" msgid "Strict - More aggressive protection" msgstr "Estricto – Protección más agresiva" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Suspender" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Sistema" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Texto" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Canales de texto" @@ -2447,6 +2700,14 @@ msgstr "El tema que se mostrará para este canal. Todos los usuarios pueden ver msgid "The user will receive an invitation to join {channelName}." msgstr "El usuario recibirá una invitación para unirse a {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "El flujo de trabajo que produjo este mensaje ya no está en el estado" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Este comando no toma parámetros." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Este campo es obligatorio" @@ -2510,6 +2771,10 @@ msgstr "Alternar notificaciones" msgid "Toggle search" msgstr "Alternar búsqueda" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Herramienta" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Tema establecido hace más de (min)" @@ -2527,6 +2792,10 @@ msgstr "Tema:" msgid "Total: {0}" msgstr "Total: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Transporte" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Fuentes de confianza" @@ -2561,6 +2830,10 @@ msgstr "Desfijar chat privado" msgid "Unpin this private message conversation" msgstr "Desfijar esta conversación de mensaje privado" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Reactivar" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Subir" @@ -2687,6 +2960,11 @@ msgstr "Muy relajado" msgid "Very Strict" msgstr "Muy estricto" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "vía @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2730,6 +3008,10 @@ msgstr "Usuario con voz" msgid "Volume" msgstr "Volumen" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Esperando el primer paso…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Fue expulsado del canal" msgid "We don't store your IRC communications on our servers" msgstr "No almacenamos tus comunicaciones IRC en nuestros servidores" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "¡Bienvenido a {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "¿Qué estás haciendo?" msgid "What this means:" msgstr "Qué significa esto:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Lo que estás haciendo" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "¿Qué piensas?" @@ -2780,6 +3070,10 @@ msgstr "¿Qué piensas?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Susurrar a un usuario en el contexto del canal actual" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Amplio – Ámbito de protección más amplio" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Historial de flujos de trabajo" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Historial de flujos de trabajo ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Procesando…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/fi/messages.mjs b/src/locales/fi/messages.mjs index c9947bc5..a890fe14 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: 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\"],\"\"],\"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! 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: 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! 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\"],\" 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/invitation-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\"],\"+fRR7i\":[\"Jäädytä\"],\"+mVPqU\":[\"Näytä Markdown-muotoilu viesteissä\"],\"+vqCJH\":[\"Tilin käyttäjänimi tunnistautumista varten\"],\"+yPBXI\":[\"Valitse tiedosto\"],\"+zy2Nq\":[\"Tyyppi\"],\"/09cao\":[\"Heikko linkkiturvallisuus (taso \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"Merkitse itsesi palanneeksi\"],\"/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ä\"],\"/xQ19T\":[\"Tämän verkon botit\"],\"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\"],\"1C/fOn\":[\"Botti ei ole vielä rekisteröinyt yhtään vinokomentoa.\"],\"1DSr1i\":[\"Rekisteröidy tilille\"],\"1O/24y\":[\"Näytä/piilota kanavaluettelo\"],\"1QfxQT\":[\"Hylkää\"],\"1VPJJ2\":[\"Ulkoisen linkin varoitus\"],\"1ZC/dv\":[\"Ei lukemattomia mainintoja tai viestejä\"],\"1pO1zi\":[\"Palvelimen nimi on pakollinen\"],\"1t/NnN\":[\"Hylkää\"],\"1uwfzQ\":[\"Näytä kanavan aihe\"],\"268g7c\":[\"Syötä näyttönimi\"],\"2F9+AZ\":[\"Raakaa IRC-liikennettä ei ole vielä tallennettu. Kokeile yhdistämistä tai viestin lähettämistä.\"],\"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\"],\"2xXP/g\":[\"Liity kanavalle\"],\"2ygf/L\":[\"← Takaisin\"],\"2zEgxj\":[\"Hae GIF-kuvia...\"],\"3JjdaA\":[\"Suorita\"],\"3NJ4MW\":[\"Avaa uudelleen työnkulku, joka tuotti tämän viestin (\",[\"stepCount\"],\" vaihetta)\"],\"3RdPhl\":[\"Nimeä kanava uudelleen\"],\"3THokf\":[\"Voice-käyttäjä\"],\"3TSz9S\":[\"Pienennä\"],\"3et0TM\":[\"Vieritä keskustelu tähän vastaukseen\"],\"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\"],\"4uKgKr\":[\"ULOS\"],\"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 ladataksesi\"],\"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ä:\"],\"9q17ZR\":[[\"0\"],\" on pakollinen.\"],\"9qIYMn\":[\"Uusi lempinimi\"],\"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?\"],\"BIDT9R\":[\"Botit\"],\"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\"],\"CaQ1Gb\":[\"Asetuksissa määritelty botti. Muuta tila muokkaamalla obbyircd.conf-tiedostoa ja suorittamalla /REHASH.\"],\"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\"],\"DSHF2K\":[\"Tämän viestin tuottanut työnkulku ei ole enää tilassa\"],\"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...\"],\"Eu7YKa\":[\"itse rekisteröitynyt\"],\"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: 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...\"],\"IcHxhR\":[\"ei verkossa\"],\"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\"],\"JMXMCX\":[\"Poissaoloviesti\"],\"JXGkhG\":[\"Muuta kanavan nimeä (vain operaattorit)\"],\"JYiL1b\":[\"yksi seuraavista:\"],\"JcD7qf\":[\"Lisää toimintoja\"],\"JdkA+c\":[\"Salainen (+s)\"],\"Jmu12l\":[\"Palvelimen kanavat\"],\"JvQ++s\":[\"Ota Markdown käyttöön\"],\"K2jwh/\":[\"WHOIS-tietoja ei saatavilla\"],\"K4vEhk\":[\"(jäädytetty)\"],\"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\"],\"KtADxr\":[\"suoritti\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" vaihe(tta) odottaa hyväksyntää\"],\"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\\\")\"],\"LYzbQ2\":[\"Työkalu\"],\"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\"],\"M45wtf\":[\"Tämä komento ei ota parametreja.\"],\"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ä\"],\"N40H+G\":[\"Kaikki\"],\"N7TQbE\":[\"Kutsu käyttäjä kanavalle \",[\"channelName\"]],\"NCca/o\":[\"Syötä oletusnimimerkki...\"],\"NQN2HS\":[\"Poista jäädytys\"],\"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+HhhG\":[\"Kuiskaa käyttäjälle nykyisen kanavan kontekstissa\"],\"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\"]],\"RlCInP\":[\"Vinokomennot\"],\"RySp6q\":[\"Piilota kommentit\"],\"RzfkXn\":[\"Vaihda lempinimesi tällä palvelimella\"],\"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\"],\"TImSWn\":[\"(ObsidianIRC käsittelee)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Merkitse itsesi poissaolevaksi\"],\"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!\"],\"UqtiKk\":[\"Sulkeutuu automaattisesti \",[\"secondsLeft\"],\" s kuluttua\"],\"UrEy4W\":[\"Avaa yksityisviesti käyttäjälle\"],\"UtUIRh\":[[\"0\"],\" vanhempaa viestiä\"],\"UwzP+U\":[\"Suojattu yhteys\"],\"V0/A4O\":[\"Kanavan omistaja\"],\"V2dwib\":[[\"0\"],\" on oltava numero.\"],\"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\"],\"XklovM\":[\"Työstetään…\"],\"Xm/s+u\":[\"Näyttö\"],\"Xp2n93\":[\"Näyttää mediaa palvelimesi luotetusta tiedostoisännästä. Ulkoisille palveluille ei lähetetä pyyntöjä.\"],\"XvjC4F\":[\"Tallennetaan...\"],\"Y+tK3n\":[\"Ensimmäinen lähetettävä viesti\"],\"Y/qryO\":[\"Hakua vastaavia käyttäjiä ei löydetty\"],\"YAqRpI\":[\"Tilin \",[\"account\"],\" rekisteröinti onnistui: \",[\"message\"]],\"YBXJ7j\":[\"SISÄÄN\"],\"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:\"],\"Z7ZXbT\":[\"Hyväksy\"],\"ZJSWfw\":[\"Viesti, joka näytetään katkaistessasi yhteyden palvelimeen\"],\"ZR1dJ4\":[\"Kutsut\"],\"ZdWg0V\":[\"Avaa selaimessa\"],\"ZhRBbl\":[\"Hae viestejä…\"],\"Zmcu3y\":[\"Lisäsuodattimet\"],\"ZqLD8l\":[\"Palvelimenlaajuinen\"],\"a2/8e5\":[\"Aihe asetettu myöhemmin kuin (min sitten)\"],\"aHKcKc\":[\"Edellinen sivu\"],\"aJTbXX\":[\"Oper-salasana\"],\"aP9gNu\":[\"tuloste katkaistu\"],\"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\"],\"bFDO8z\":[\"gateway verkossa\"],\"bQ6BJn\":[\"Määritä yksityiskohtaiset tulvasuojaussäännöt. Kukin sääntö määrittää, mitä toimintaa seurataan ja mitä tehdään kun raja-arvot ylitetään.\"],\"bVBC/W\":[\"Gateway yhdistetty\"],\"beV7+y\":[\"Käyttäjä saa kutsun liittyä kanavalle \",[\"channelName\"],\".\"],\"bk84cH\":[\"Poissaoloviesti\"],\"bkHdLj\":[\"Lisää IRC-palvelin\"],\"bmQLn5\":[\"Lisää sääntö\"],\"bv4cFj\":[\"Siirtotapa\"],\"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\"],\"\"],\"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\"],\"dRqrdL\":[[\"0\"],\" on oltava kokonaisluku.\"],\"dXqxlh\":[\"<0>⚠️ Tietoturvariski! Tämä yhteys voi olla alttiina salakuuntelulle tai välimieshyökkäyksille.\"],\"da9Q/R\":[\"Muutti kanavan asetuksia\"],\"dhJN3N\":[\"Näytä kommentit\"],\"dj2xTE\":[\"Hylkää ilmoitus\"],\"dnUmOX\":[\"Tähän verkkoon ei ole vielä rekisteröity botteja.\"],\"dpCzmC\":[\"Tulvasuojausasetukset\"],\"e7KzRG\":[[\"0\"],\" vaihe(tta)\"],\"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: 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.\"],\"fYdEvu\":[\"Työnkulkuhistoria (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Valitse botti vasemmalta nähdäksesi sen komennot ja hallintatoiminnot.\"],\"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\"],\"jUXib7\":[\"Vastausviesti ei ole enää näkyvissä\"],\"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:\"],\"kG2fiE\":[\"asetuksissa määritelty\"],\"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.\"],\"ky2mw7\":[\"kautta @\",[\"0\"]],\"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ä\"],\"lH+ed1\":[\"Odotetaan ensimmäistä vaihetta…\"],\"lHy8N5\":[\"Ladataan lisää kanavia...\"],\"lasgrr\":[\"käytetty\"],\"lbpf14\":[\"Liity kanavaan \",[\"value\"]],\"lf3MT4\":[\"Kanava, jolta poistutaan (oletuksena nykyinen)\"],\"lfFsZ4\":[\"Kanavat\"],\"lkNdiH\":[\"Tilin nimi\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Lataa kuva\"],\"loQxaJ\":[\"Olen takaisin\"],\"lvfaxv\":[\"KOTI\"],\"m16xKo\":[\"Lisää\"],\"m8flAk\":[\"Esikatselu (ei vielä lähetetty)\"],\"mDkV0w\":[\"Käynnistetään työnkulkua…\"],\"mEPxTp\":[\"<0>⚠️ Ole varovainen! Avaa linkkejä vain luotetuista lähteistä. Haitalliset linkit voivat vaarantaa tietoturvasi tai yksityisyytesi.\"],\"mHGdhG\":[\"Palvelimen tiedot\"],\"mHS8lb\":[\"Viesti #\",[\"0\"]],\"mHfd/S\":[\"Mitä olet tekemässä\"],\"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\"],\"nX4XLG\":[\"Operaattoritoiminnot\"],\"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\"],\"oaTtrx\":[\"Etsi botteja\"],\"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\"],\"pQBYsE\":[\"Vastasi keskustelussa\"],\"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ä\"],\"qXgujk\":[\"Lähetä toiminto / emote\"],\"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\"],\" on seuraavia tietoturvaongelmia:\"],\"sGH11W\":[\"Palvelin\"],\"sHI1H+\":[\"on nyt tunnettu nimellä **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" kutsui sinut liittymään kanavalle \",[\"channel\"]],\"sW5OjU\":[\"pakollinen\"],\"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\"],\"thC9Rq\":[\"Poistu kanavalta\"],\"tiBsJk\":[\"poistui kanavalta \",[\"channelName\"]],\"tt4/UD\":[\"poistui (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Kanava, jolle liitytään (#nimi)\"],\"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\"]],\"uMIUx8\":[\"Poistetaanko botti \",[\"0\"],\"? Tämä poistaa tietokantarivin pehmeästi; käytä nimimerkkiä uudelleen vasta /REHASH-komennon jälkeen.\"],\"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\"],\"wCKe3+\":[\"Työnkulkuhistoria\"],\"wFjjxZ\":[\"potkittiin kanavalta \",[\"channelName\"],\" käyttäjän \",[\"username\"],\" toimesta (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Porttikieltopoikkeuksia ei löydetty\"],\"wPrGnM\":[\"Kanavan ylläpitäjä\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Päättely\"],\"wbm86v\":[\"Näytä kun käyttäjät liittyvät kanavalle tai poistuvat\"],\"wdxz7K\":[\"Lähde\"],\"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/invitation-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\"],\"xeiujy\":[\"Teksti\"],\"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\"]],\"zPBDzU\":[\"Peruuta työnkulku\"],\"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..6d1504d0 100644 --- a/src/locales/fi/messages.po +++ b/src/locales/fi/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Viedään IRC tulevaisuuteen" msgid "— open in viewer" msgstr "— avaa katseluohjelmassa" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(ObsidianIRC käsittelee)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(jäädytetty)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Näytä 1 kohde lisää} other {Näytä {1} kohdetta li msgid "{0} and {1} are typing..." msgstr "{0} ja {1} kirjoittavat..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} on pakollinen." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} kirjoittaa..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} on oltava numero." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} on oltava kokonaisluku." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} vanhempaa viestiä" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} vaihe(tta)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} vaihe(tta) odottaa hyväksyntää" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Lisäsuodattimet" msgid "Album" msgstr "Albumi" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Kaikki" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Kaikki sisältö" @@ -297,6 +334,12 @@ msgstr "Käytä suodattimet ja päivitä" msgid "Applying..." msgstr "Käytetään..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Hyväksy" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Tunnistautunut tili" msgid "Auto Fallback to Single Line" msgstr "Automaattinen palautuminen yksiriviseksi" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Sulkeutuu automaattisesti {secondsLeft} s kuluttua" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Tunnistaudu automaattisesti operaattoriksi yhdistäessä" @@ -359,6 +406,10 @@ msgstr "Poissa" msgid "Away from keyboard" msgstr "Poissa näppäimistöltä" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Poissaoloviesti" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Poissaoloviesti" msgid "Away:" msgstr "Poissa:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Estot" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Botti" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Botti ei ole vielä rekisteröinyt yhtään vinokomentoa." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Botit" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Tämän verkon botit" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Selaa kaikkia palvelimen kanavia" @@ -430,6 +499,7 @@ msgstr "Selaa kaikkia palvelimen kanavia" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Peruuta yhteys" msgid "Cancel reply" msgstr "Peruuta vastaus" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Peruuta työnkulku" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Muuta kanavan nimeä (vain operaattorit)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Vaihda lempinimesi tällä palvelimella" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Muutti kanavan asetuksia" @@ -459,6 +537,7 @@ msgstr "Vaihtoi nimimerkin" msgid "changed the topic to: {topic}" msgstr "vaihtoi aiheen: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Kanava" @@ -519,6 +598,14 @@ msgstr "Kanavan omistaja" msgid "Channel Settings" msgstr "Kanavan asetukset" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Kanava, jolle liitytään (#nimi)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Kanava, jolta poistutaan (oletuksena nykyinen)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Kanavan aihe" @@ -531,6 +618,7 @@ msgstr "Kanava ei näy LIST- tai NAMES-komennoissa" msgid "channel-name" msgstr "kanavan-nimi" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Kanavat" @@ -606,6 +694,9 @@ msgstr "Käyttäjäraja (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Kommentit" msgid "Comments ({commentCount})" msgstr "Kommentit ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "asetuksissa määritelty" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Asetuksissa määritelty botti. Muuta tila muokkaamalla obbyircd.conf-tiedostoa ja suorittamalla /REHASH." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "Määritä yksityiskohtaiset tulvasuojaussäännöt. Kukin sääntö määrittää, mitä toimintaa seurataan ja mitä tehdään kun raja-arvot ylitetään." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Oletusnimimerkki" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Poista" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Poistetaanko botti {0}? Tämä poistaa tietokantarivin pehmeästi; käytä nimimerkkiä uudelleen vasta /REHASH-komennon jälkeen." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Poista kanava" @@ -843,6 +948,10 @@ msgstr "Selaa" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Tutustu IRC:n maailmaan ObsidianIRC:llä" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Hylkää" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Hylkää ilmoitus" @@ -891,7 +1000,7 @@ msgstr "Lataa" #: src/components/layout/ChatArea.tsx msgid "Drop files to upload" -msgstr "Pudota tiedostot lähettääksesi" +msgstr "Pudota tiedostot ladataksesi" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Suodata kanavia..." msgid "Filter members…" msgstr "Suodata jäseniä…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Ensimmäinen lähetettävä viesti" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Tulvasuojausprofiili (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (maailmanlaajuinen esto)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway yhdistetty" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway verkossa" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Yleiset" @@ -1186,6 +1307,10 @@ msgstr "Kuva {0}/{1}" msgid "Image preview" msgstr "Kuvan esikatselu" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "SISÄÄN" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Tiedot:" @@ -1270,6 +1395,10 @@ msgstr "Liity kanavalle {0}" msgid "Join {value}" msgstr "Liity kanavaan {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Liity kanavalle" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Lue lisää mukautetuista säännöistä →" msgid "Learn more about profiles →" msgstr "Lue lisää profiileista →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Poistu kanavalta" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Poistu kanavalta" @@ -1411,6 +1544,14 @@ msgstr "Hallinnoi profiilitietojasi ja metatietoja" msgid "Mark as bot - usually 'on' or empty" msgstr "Merkitse botiksi – yleensä 'on' tai tyhjä" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Merkitse itsesi poissaolevaksi" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Merkitse itsesi palanneeksi" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Enimmäiskäyttäjämäärä" @@ -1573,6 +1714,10 @@ msgstr "Verkon nimi" msgid "New DM" msgstr "Uusi DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Uusi lempinimi" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Seuraava kuva" @@ -1617,6 +1762,10 @@ msgstr "Porttikieltopoikkeuksia ei löydetty" msgid "No bans found" msgstr "Estoja ei löydetty" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Tähän verkkoon ei ole vielä rekisteröity botteja." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Ei keskuspalvelinta:" @@ -1672,7 +1821,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. Kokeile yhdistämistä tai viestin lähettämistä." #: src/components/ui/QuickActions.tsx msgid "No results found" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC – IRC:n tulevaisuuteen" msgid "Off" msgstr "Pois" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "ei verkossa" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Poissa verkosta" @@ -1754,6 +1908,10 @@ msgstr "Poissa verkosta" msgid "on" msgstr "päälle" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "yksi seuraavista:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Hups! Verkon jako! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Avaa yksityisviesti käyttäjälle" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Avaa kanavan asetukset" @@ -1828,10 +1990,23 @@ msgstr "Oper-salasana" msgid "Oper Username" msgstr "Oper-käyttäjänimi" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Operaattoritoiminnot" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Valinnaiset kaatumisraportit sovelluksen parantamiseksi" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "ULOS" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "tuloste katkaistu" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Omistaja" @@ -1994,6 +2169,10 @@ msgstr "Lähtöviesti" msgid "Quit the server" msgstr "Poistui palvelimelta" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "suoritti" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Reagoi" @@ -2024,6 +2203,10 @@ msgstr "Syy" msgid "Reason (optional)" msgstr "Syy (valinnainen)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Päättely" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Yhdistä uudelleen palvelimeen" @@ -2037,6 +2220,11 @@ msgstr "Päivitä" msgid "Register for an account" msgstr "Rekisteröidy tilille" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Hylkää" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Löysä" @@ -2077,11 +2265,27 @@ msgstr "Nimeä tämä kanava uudelleen palvelimella. Kaikki käyttäjät näkev msgid "Render markdown formatting in messages" msgstr "Näytä Markdown-muotoilu viesteissä" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Avaa uudelleen työnkulku, joka tuotti tämän viestin ({stepCount} vaihetta)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Vastaa" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "pakollinen" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Vastasi keskustelussa" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Vastausviesti ei ole enää näkyvissä" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Yritä lähettää uudelleen" msgid "Rules" msgstr "Säännöt" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Suorita" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Turvallinen" @@ -2121,6 +2329,10 @@ msgstr "Tallennettu" msgid "Saving..." msgstr "Tallennetaan..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Vieritä keskustelu tähän vastaukseen" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Siirry loppuun" msgid "Search" msgstr "Hae" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Etsi botteja" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Hae GIF-kuvia aloittaaksesi" @@ -2183,6 +2399,10 @@ msgstr "Tietoturvavaroitus" msgid "Seek" msgstr "Selaa" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Valitse botti vasemmalta nähdäksesi sen komennot ja hallintatoiminnot." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Valitse kanava" @@ -2195,6 +2415,10 @@ msgstr "Valitse jäsen" msgid "Select or add a channel to get started." msgstr "Valitse kanava tai lisää uusi päästäksesi alkuun." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "itse rekisteröitynyt" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Lähetä GIF" msgid "Send a warning message to {username}" msgstr "Lähetä varoitusviesti käyttäjälle {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Lähetä toiminto / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Lähetä kutsu" @@ -2280,6 +2508,10 @@ msgstr "Palvelimen salasana" msgid "Server-to-server communication may use unencrypted connections" msgstr "Palvelimien välinen viestintä voi käyttää salaamattomia yhteyksiä" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "Palvelimenlaajuinen" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Aseta aihe…" @@ -2376,6 +2608,10 @@ msgstr "Näyttää mediaa palvelimesi luotetusta tiedostoisännästä. Ulkoisill msgid "Signed On" msgstr "Kirjautunut" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Vinokomennot" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Ohjelmisto:" @@ -2392,10 +2628,18 @@ msgstr "Lajittele käyttäjämäärän mukaan" msgid "SoundCloud player" msgstr "SoundCloud-soitin" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Lähde" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Aloita yksityiskeskustelu" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Käynnistetään työnkulkua…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Tilaviestit" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Pysäytä" @@ -2419,10 +2664,18 @@ msgstr "Tiukka" msgid "Strict - More aggressive protection" msgstr "Tiukka – aggressiivisempi suojaus" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Jäädytä" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Järjestelmä" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Teksti" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Tekstikanavat" @@ -2447,6 +2700,14 @@ msgstr "Aihe, joka näytetään tälle kanavalle. Kaikki käyttäjät voivat nä msgid "The user will receive an invitation to join {channelName}." msgstr "Käyttäjä saa kutsun liittyä kanavalle {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Tämän viestin tuottanut työnkulku ei ole enää tilassa" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Tämä komento ei ota parametreja." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Tämä kenttä on pakollinen" @@ -2510,6 +2771,10 @@ msgstr "Näytä/piilota ilmoitukset" msgid "Toggle search" msgstr "Näytä/piilota haku" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Työkalu" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Aihe asetettu myöhemmin kuin (min sitten)" @@ -2527,6 +2792,10 @@ msgstr "Aihe:" msgid "Total: {0}" msgstr "Yhteensä: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Siirtotapa" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Luotetut lähteet" @@ -2561,6 +2830,10 @@ msgstr "Irrota yksityiskeskustelu" msgid "Unpin this private message conversation" msgstr "Irrota tämä yksityisviestiketju" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Poista jäädytys" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Lähetä" @@ -2687,6 +2960,11 @@ msgstr "Hyvin löysä" msgid "Very Strict" msgstr "Hyvin tiukka" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "kautta @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2730,6 +3008,10 @@ msgstr "Voice-käyttäjä" msgid "Volume" msgstr "Äänenvoimakkuus" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Odotetaan ensimmäistä vaihetta…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Poistettiin kanavalta" msgid "We don't store your IRC communications on our servers" msgstr "Emme tallenna IRC-viestintääsi palvelimillemme" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Tervetuloa palvelimeen {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Mitä olet tekemässä?" msgid "What this means:" msgstr "Mitä tämä tarkoittaa:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Mitä olet tekemässä" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "Mitä ajattelet?" @@ -2780,6 +3070,10 @@ msgstr "Mitä ajattelet?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Kuiskaa käyttäjälle nykyisen kanavan kontekstissa" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Laaja – laajempi suojauslaajuus" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Työnkulkuhistoria" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Työnkulkuhistoria ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Työstetään…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/fr/messages.mjs b/src/locales/fr/messages.mjs index 26d689bd..9c4d787f 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 : 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\"],\"\"],\"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é ! 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 : 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 ! 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\"],\" 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/invitationn'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\"],\"+fRR7i\":[\"Suspendre\"],\"+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\"],\")\"],\"/0mUpR\":[\"Vous marquer comme de retour\"],\"/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\"],\"/xQ19T\":[\"Bots sur ce réseau\"],\"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\"],\"1C/fOn\":[\"Le bot n'a pas encore enregistré de commandes slash.\"],\"1DSr1i\":[\"Créer un compte\"],\"1O/24y\":[\"Afficher/masquer la liste des canaux\"],\"1QfxQT\":[\"Ignorer\"],\"1VPJJ2\":[\"Avertissement de lien externe\"],\"1ZC/dv\":[\"Aucune mention ou message non lu\"],\"1pO1zi\":[\"Le nom du serveur est requis\"],\"1t/NnN\":[\"Rejeter\"],\"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\"],\"2xXP/g\":[\"Rejoindre un salon\"],\"2ygf/L\":[\"← Retour\"],\"2zEgxj\":[\"Rechercher des GIFs...\"],\"3JjdaA\":[\"Exécuter\"],\"3NJ4MW\":[\"Rouvrir le workflow qui a produit ce message (\",[\"stepCount\"],\" étapes)\"],\"3RdPhl\":[\"Renommer le canal\"],\"3THokf\":[\"Utilisateur avec droit de parole\"],\"3TSz9S\":[\"Réduire\"],\"3et0TM\":[\"Faire défiler le chat jusqu'à cette réponse\"],\"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\"],\"4uKgKr\":[\"SORTIE\"],\"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 à 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 :\"],\"9q17ZR\":[[\"0\"],\" est obligatoire.\"],\"9qIYMn\":[\"Nouveau pseudo\"],\"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 ?\"],\"BIDT9R\":[\"Bots\"],\"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\"],\"CaQ1Gb\":[\"Bot défini par configuration. Modifiez obbyircd.conf et /REHASH pour changer son état.\"],\"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é\"],\"DSHF2K\":[\"Le workflow qui a produit ce message n'est plus dans l'état\"],\"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...\"],\"Eu7YKa\":[\"auto-enregistré\"],\"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 : 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...\"],\"IcHxhR\":[\"hors ligne\"],\"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\"],\"JMXMCX\":[\"Message d'absence\"],\"JXGkhG\":[\"Changer le nom du canal (opérateurs uniquement)\"],\"JYiL1b\":[\"l'un de :\"],\"JcD7qf\":[\"Plus d'actions\"],\"JdkA+c\":[\"Secret (+s)\"],\"Jmu12l\":[\"Canaux du serveur\"],\"JvQ++s\":[\"Activer le Markdown\"],\"K2jwh/\":[\"Aucune donnée WHOIS disponible\"],\"K4vEhk\":[\"(suspendu)\"],\"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\"],\"KtADxr\":[\"a exécuté\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" étape(s) en attente d'approbation\"],\"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 »)\"],\"LYzbQ2\":[\"Outil\"],\"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\"],\"M45wtf\":[\"Cette commande ne prend pas de paramètres.\"],\"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\"],\"N40H+G\":[\"Tous\"],\"N7TQbE\":[\"Inviter un utilisateur dans \",[\"channelName\"]],\"NCca/o\":[\"Entrez le pseudo par défaut...\"],\"NQN2HS\":[\"Réactiver\"],\"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+HhhG\":[\"Chuchoter à un utilisateur dans le contexte du salon actuel\"],\"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\"]],\"RlCInP\":[\"Commandes slash\"],\"RySp6q\":[\"Masquer les commentaires\"],\"RzfkXn\":[\"Changer votre pseudo sur ce serveur\"],\"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\"],\"TImSWn\":[\"(géré par ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Vous marquer comme absent\"],\"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 !\"],\"UqtiKk\":[\"Masquage automatique dans \",[\"secondsLeft\"],\"s\"],\"UrEy4W\":[\"Ouvrir un message privé avec un utilisateur\"],\"UtUIRh\":[[\"0\"],\" anciens messages\"],\"UwzP+U\":[\"Connexion sécurisée\"],\"V0/A4O\":[\"Propriétaire du canal\"],\"V2dwib\":[[\"0\"],\" doit être un nombre.\"],\"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\"],\"XklovM\":[\"Traitement…\"],\"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+tK3n\":[\"Premier message à envoyer\"],\"Y/qryO\":[\"Aucun utilisateur ne correspond à votre recherche\"],\"YAqRpI\":[\"Enregistrement du compte réussi pour \",[\"account\"],\" : \",[\"message\"]],\"YBXJ7j\":[\"ENTRÉE\"],\"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 :\"],\"Z7ZXbT\":[\"Approuver\"],\"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\"],\"ZqLD8l\":[\"À l'échelle du serveur\"],\"a2/8e5\":[\"Sujet défini après (min)\"],\"aHKcKc\":[\"Page précédente\"],\"aJTbXX\":[\"Mot de passe Oper\"],\"aP9gNu\":[\"sortie tronquée\"],\"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\"],\"bFDO8z\":[\"gateway en ligne\"],\"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.\"],\"bVBC/W\":[\"Gateway connecté\"],\"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\"],\"cde3ce\":[\"Message <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\":[\"à\"],\"dRqrdL\":[[\"0\"],\" doit être un nombre entier.\"],\"dXqxlh\":[\"<0>⚠️ Risque de sécurité ! 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\"],\"dnUmOX\":[\"Aucun bot enregistré sur ce réseau pour le moment.\"],\"dpCzmC\":[\"Paramètres de protection contre le flood\"],\"e7KzRG\":[[\"0\"],\" étape(s)\"],\"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 : 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é.\"],\"fYdEvu\":[\"Historique des workflows (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Sélectionnez un bot à gauche pour voir ses commandes et ses actions de gestion.\"],\"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\"],\"jUXib7\":[\"Le message de réponse n'est plus visible\"],\"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 :\"],\"kG2fiE\":[\"défini par configuration\"],\"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.\"],\"ky2mw7\":[\"via @\",[\"0\"]],\"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\"],\"lH+ed1\":[\"En attente de la première étape…\"],\"lHy8N5\":[\"Chargement de canaux supplémentaires...\"],\"lasgrr\":[\"utilisé\"],\"lbpf14\":[\"Rejoindre \",[\"value\"]],\"lf3MT4\":[\"Salon à quitter (par défaut : actuel)\"],\"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é)\"],\"mDkV0w\":[\"Démarrage du workflow…\"],\"mEPxTp\":[\"<0>⚠️ Attention ! 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\"]],\"mHfd/S\":[\"Ce que vous faites\"],\"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\"],\"nX4XLG\":[\"Actions d'opérateur\"],\"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\"],\"oaTtrx\":[\"Rechercher des bots\"],\"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\"],\"pQBYsE\":[\"A répondu dans le chat\"],\"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\"],\"qXgujk\":[\"Envoyer une action / emote\"],\"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\"],\" 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\"]],\"sW5OjU\":[\"requis\"],\"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\"],\"thC9Rq\":[\"Quitter un salon\"],\"tiBsJk\":[\"a quitté \",[\"channelName\"]],\"tt4/UD\":[\"a quitté (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Salon à rejoindre (#nom)\"],\"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\"]],\"uMIUx8\":[\"Supprimer le bot \",[\"0\"],\" ? Cela supprime la ligne de la base de données de façon réversible ; réutilisez le pseudo plus tard uniquement après un /REHASH.\"],\"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\"],\"wCKe3+\":[\"Historique des workflows\"],\"wFjjxZ\":[\"a été expulsé de \",[\"channelName\"],\" par \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Aucune exception de bannissement trouvée\"],\"wPrGnM\":[\"Administrateur du canal\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Raisonnement\"],\"wbm86v\":[\"Afficher quand des utilisateurs rejoignent ou quittent des salons\"],\"wdxz7K\":[\"Source\"],\"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/invitationn'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\"],\"xeiujy\":[\"Texte\"],\"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\"]],\"zPBDzU\":[\"Annuler le workflow\"],\"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..818b7000 100644 --- a/src/locales/fr/messages.po +++ b/src/locales/fr/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Amener IRC vers le futur" msgid "— open in viewer" msgstr "— ouvrir dans le visualiseur" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(géré par ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(suspendu)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Afficher 1 élément de plus} other {Afficher {1} élé msgid "{0} and {1} are typing..." msgstr "{0} et {1} sont en train d'écrire..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} est obligatoire." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} est en train d'écrire..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} doit être un nombre." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} doit être un nombre entier." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} anciens messages" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} étape(s)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} étape(s) en attente d'approbation" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Filtres avancés" msgid "Album" msgstr "Album" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Tous" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Tout le contenu" @@ -297,6 +334,12 @@ msgstr "Appliquer les filtres & Actualiser" msgid "Applying..." msgstr "Application en cours..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Approuver" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Compte authentifié" msgid "Auto Fallback to Single Line" msgstr "Retour automatique à une seule ligne" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Masquage automatique dans {secondsLeft}s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "S'authentifier automatiquement comme opérateur à la connexion" @@ -359,6 +406,10 @@ msgstr "Absent" msgid "Away from keyboard" msgstr "Absent du clavier" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Message d'absence" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Message d'absence" msgid "Away:" msgstr "Absent :" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Bannissements" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Le bot n'a pas encore enregistré de commandes slash." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Bots" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Bots sur ce réseau" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Parcourir tous les canaux du serveur" @@ -430,6 +499,7 @@ msgstr "Parcourir tous les canaux du serveur" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Annuler la connexion" msgid "Cancel reply" msgstr "Annuler la réponse" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Annuler le workflow" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Changer le nom du canal (opérateurs uniquement)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Changer votre pseudo sur ce serveur" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Modes du canal modifiés" @@ -459,6 +537,7 @@ msgstr "Pseudo modifié" msgid "changed the topic to: {topic}" msgstr "a changé le sujet en : {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Canal" @@ -519,6 +598,14 @@ msgstr "Propriétaire du canal" msgid "Channel Settings" msgstr "Paramètres du canal" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Salon à rejoindre (#nom)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Salon à quitter (par défaut : actuel)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Sujet du salon" @@ -531,6 +618,7 @@ msgstr "Le salon n'apparaîtra pas dans les commandes LIST ou NAMES" msgid "channel-name" msgstr "nom-du-salon" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Canaux" @@ -606,6 +694,9 @@ msgstr "Limite de clients (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Commentaires" msgid "Comments ({commentCount})" msgstr "Commentaires ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "défini par configuration" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Bot défini par configuration. Modifiez obbyircd.conf et /REHASH pour changer son état." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "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." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Pseudo par défaut" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Supprimer" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Supprimer le bot {0} ? Cela supprime la ligne de la base de données de façon réversible ; réutilisez le pseudo plus tard uniquement après un /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Supprimer le canal" @@ -843,6 +948,10 @@ msgstr "Découvrir" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Découvrez le monde de l'IRC avec ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Ignorer" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Ignorer la notification" @@ -891,7 +1000,7 @@ 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 à téléverser" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Filtrer les canaux..." msgid "Filter members…" msgstr "Filtrer les membres…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Premier message à envoyer" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Profil de flood (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (bannissement global)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway connecté" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway en ligne" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Général" @@ -1186,6 +1307,10 @@ msgstr "Image {0} sur {1}" msgid "Image preview" msgstr "Aperçu de l'image" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "ENTRÉE" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info :" @@ -1270,6 +1395,10 @@ msgstr "Rejoindre {0}" msgid "Join {value}" msgstr "Rejoindre {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Rejoindre un salon" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "En savoir plus sur les règles personnalisées →" msgid "Learn more about profiles →" msgstr "En savoir plus sur les profils →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Quitter un salon" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Quitter le canal" @@ -1411,6 +1544,14 @@ msgstr "Gérez les informations de votre profil et vos métadonnées" msgid "Mark as bot - usually 'on' or empty" msgstr "Marquer comme bot, généralement 'on' ou vide" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Vous marquer comme absent" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Vous marquer comme de retour" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Utilisateurs max." @@ -1573,6 +1714,10 @@ msgstr "Nom du réseau" msgid "New DM" msgstr "Nouveau message privé" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Nouveau pseudo" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Image suivante" @@ -1617,6 +1762,10 @@ msgstr "Aucune exception de bannissement trouvée" msgid "No bans found" msgstr "Aucun bannissement trouvé" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Aucun bot enregistré sur ce réseau pour le moment." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Pas de serveur central :" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - L'IRC vers le futur" msgid "Off" msgstr "Désactivé" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "hors ligne" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Hors ligne" @@ -1754,6 +1908,10 @@ msgstr "Hors ligne" msgid "on" msgstr "activé" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "l'un de :" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Oups ! La réseau s'est divisé ! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Ouvrir un message privé avec un utilisateur" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Ouvrir les paramètres de configuration du canal" @@ -1828,10 +1990,23 @@ msgstr "Mot de passe Oper" msgid "Oper Username" msgstr "Nom d'utilisateur opérateur" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Actions d'opérateur" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Rapports de plantage optionnels pour améliorer l'application" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "SORTIE" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "sortie tronquée" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Propriétaire" @@ -1994,6 +2169,10 @@ msgstr "Message de déconnexion" msgid "Quit the server" msgstr "A quitté le serveur" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "a exécuté" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Réagir" @@ -2024,6 +2203,10 @@ msgstr "Raison" msgid "Reason (optional)" msgstr "Raison (facultatif)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Raisonnement" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Se reconnecter au serveur" @@ -2037,6 +2220,11 @@ msgstr "Actualiser" msgid "Register for an account" msgstr "Créer un compte" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Rejeter" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Détendu" @@ -2077,11 +2265,27 @@ msgstr "Renommer ce salon sur le serveur. Tous les utilisateurs verront le nouve msgid "Render markdown formatting in messages" msgstr "Afficher le formatage Markdown dans les messages" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Rouvrir le workflow qui a produit ce message ({stepCount} étapes)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Répondre" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "requis" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "A répondu dans le chat" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Le message de réponse n'est plus visible" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Réessayer l'envoi" msgid "Rules" msgstr "Règles" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Exécuter" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Sécurisé" @@ -2121,6 +2329,10 @@ msgstr "Enregistré" msgid "Saving..." msgstr "Enregistrement..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Faire défiler le chat jusqu'à cette réponse" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Défiler vers le bas" msgid "Search" msgstr "Rechercher" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Rechercher des bots" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Recherchez des GIFs pour commencer" @@ -2183,6 +2399,10 @@ msgstr "Avertissement de sécurité" msgid "Seek" msgstr "Avancer" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Sélectionnez un bot à gauche pour voir ses commandes et ses actions de gestion." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Sélectionner un canal" @@ -2195,6 +2415,10 @@ msgstr "Sélectionner un membre" msgid "Select or add a channel to get started." msgstr "Sélectionnez ou ajoutez un canal pour commencer." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "auto-enregistré" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Envoyer un GIF" msgid "Send a warning message to {username}" msgstr "Envoyer un message d'avertissement à {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Envoyer une action / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Envoyer l'invitation" @@ -2280,6 +2508,10 @@ msgstr "Mot de passe du serveur" msgid "Server-to-server communication may use unencrypted connections" msgstr "Les communications entre serveurs peuvent utiliser des connexions non chiffrées" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "À l'échelle du serveur" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Définir un sujet…" @@ -2376,6 +2608,10 @@ msgstr "Affiche les médias provenant de l'hébergeur de fichiers de confiance d msgid "Signed On" msgstr "Connecté depuis" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Commandes slash" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Logiciel :" @@ -2392,10 +2628,18 @@ msgstr "Trier par utilisateurs" msgid "SoundCloud player" msgstr "Lecteur SoundCloud" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Source" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Démarrer un message privé" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Démarrage du workflow…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Messages de statut" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Arrêter" @@ -2419,10 +2664,18 @@ msgstr "Strict" msgid "Strict - More aggressive protection" msgstr "Strict – Protection plus agressive" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Suspendre" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Système" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Texte" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Salons textuels" @@ -2447,6 +2700,14 @@ msgstr "Le sujet affiché pour ce salon. Tous les utilisateurs peuvent le voir." msgid "The user will receive an invitation to join {channelName}." msgstr "L'utilisateur recevra une invitation à rejoindre {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Le workflow qui a produit ce message n'est plus dans l'état" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Cette commande ne prend pas de paramètres." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Ce champ est obligatoire" @@ -2510,6 +2771,10 @@ msgstr "Activer/désactiver les notifications" msgid "Toggle search" msgstr "Activer/désactiver la recherche" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Outil" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Sujet défini après (min)" @@ -2527,6 +2792,10 @@ msgstr "Sujet :" msgid "Total: {0}" msgstr "Total : {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Transport" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Sources de confiance" @@ -2561,6 +2830,10 @@ msgstr "Désépingler la conversation privée" msgid "Unpin this private message conversation" msgstr "Désépingler cette conversation privée" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Réactiver" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Téléverser" @@ -2687,6 +2960,11 @@ msgstr "Très détendu" msgid "Very Strict" msgstr "Très strict" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "via @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Vidéo" @@ -2730,6 +3008,10 @@ msgstr "Utilisateur avec droit de parole" msgid "Volume" msgstr "Volume" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "En attente de la première étape…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "A été expulsé du canal" msgid "We don't store your IRC communications on our servers" msgstr "Nous ne stockons pas vos communications IRC sur nos serveurs" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Bienvenue sur {__DEFAULT_IRC_SERVER_NAME__} !" @@ -2772,6 +3058,10 @@ msgstr "Qu'est-ce que vous faites ?" msgid "What this means:" msgstr "Ce que cela signifie :" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Ce que vous faites" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "À quoi pensez-vous ?" @@ -2780,6 +3070,10 @@ msgstr "À quoi pensez-vous ?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Chuchoter à un utilisateur dans le contexte du salon actuel" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Large – Portée de protection étendue" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Historique des workflows" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Historique des workflows ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Traitement…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/it/messages.mjs b/src/locales/it/messages.mjs index 2f027263..4542153a 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: 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\"],\"\"],\"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! 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: 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! 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\"],\" 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/invitationnon è 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\"],\"+fRR7i\":[\"Sospendi\"],\"+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\"],\")\"],\"/0mUpR\":[\"Segnati come tornato\"],\"/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\"],\"/xQ19T\":[\"Bot su questa rete\"],\"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\"],\"1C/fOn\":[\"Il bot non ha ancora registrato alcun comando slash.\"],\"1DSr1i\":[\"Registra un account\"],\"1O/24y\":[\"Attiva/Disattiva lista canali\"],\"1QfxQT\":[\"Ignora\"],\"1VPJJ2\":[\"Avviso link esterno\"],\"1ZC/dv\":[\"Nessuna menzione o messaggio non letto\"],\"1pO1zi\":[\"Il nome del server è obbligatorio\"],\"1t/NnN\":[\"Rifiuta\"],\"1uwfzQ\":[\"Visualizza topic del canale\"],\"268g7c\":[\"Inserisci nome visualizzato\"],\"2F9+AZ\":[\"Nessun traffico IRC grezzo 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\"],\"2xXP/g\":[\"Unisciti a un canale\"],\"2ygf/L\":[\"← Indietro\"],\"2zEgxj\":[\"Cerca GIF...\"],\"3JjdaA\":[\"Esegui\"],\"3NJ4MW\":[\"Riapri il workflow che ha prodotto questo messaggio (\",[\"stepCount\"],\" passaggi)\"],\"3RdPhl\":[\"Rinomina canale\"],\"3THokf\":[\"Utente con diritto di parola\"],\"3TSz9S\":[\"Minimizza\"],\"3et0TM\":[\"Scorri la chat fino a questa risposta\"],\"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\"],\"4uKgKr\":[\"OUT\"],\"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\":[\"Trascina qui i file da caricare\"],\"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:\"],\"9q17ZR\":[[\"0\"],\" è obbligatorio.\"],\"9qIYMn\":[\"Nuovo nickname\"],\"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?\"],\"BIDT9R\":[\"Bot\"],\"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\"],\"CaQ1Gb\":[\"Bot definito da configurazione. Modifica obbyircd.conf e usa /REHASH per cambiarne lo stato.\"],\"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\"],\"DSHF2K\":[\"Il workflow che ha prodotto questo messaggio non è più nello stato\"],\"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...\"],\"Eu7YKa\":[\"auto-registrato\"],\"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: 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...\"],\"IcHxhR\":[\"offline\"],\"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\"],\"JMXMCX\":[\"Messaggio di assenza\"],\"JXGkhG\":[\"Cambia il nome del canale (solo operatori)\"],\"JYiL1b\":[\"uno di:\"],\"JcD7qf\":[\"Altre azioni\"],\"JdkA+c\":[\"Segreto (+s)\"],\"Jmu12l\":[\"Canali del server\"],\"JvQ++s\":[\"Abilita Markdown\"],\"K2jwh/\":[\"Nessun dato WHOIS disponibile\"],\"K4vEhk\":[\"(sospeso)\"],\"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\"],\"KtADxr\":[\"ha eseguito\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" passaggio/i in attesa di approvazione\"],\"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\\\")\"],\"LYzbQ2\":[\"Strumento\"],\"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\"],\"M45wtf\":[\"Questo comando non accetta parametri.\"],\"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\"],\"N40H+G\":[\"Tutti\"],\"N7TQbE\":[\"Invita utente in \",[\"channelName\"]],\"NCca/o\":[\"Inserisci nickname predefinito...\"],\"NQN2HS\":[\"Riattiva\"],\"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+HhhG\":[\"Sussurra a un utente nel contesto del canale corrente\"],\"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\"]],\"RlCInP\":[\"Comandi slash\"],\"RySp6q\":[\"Nascondi commenti\"],\"RzfkXn\":[\"Cambia il tuo nickname su questo server\"],\"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\"],\"TImSWn\":[\"(gestito da ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Segnati come assente\"],\"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!\"],\"UqtiKk\":[\"Chiusura automatica tra \",[\"secondsLeft\"],\"s\"],\"UrEy4W\":[\"Apri un messaggio privato a un utente\"],\"UtUIRh\":[[\"0\"],\" messaggi precedenti\"],\"UwzP+U\":[\"Connessione sicura\"],\"V0/A4O\":[\"Proprietario del canale\"],\"V2dwib\":[[\"0\"],\" deve essere un numero.\"],\"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\"],\"XklovM\":[\"In corso…\"],\"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+tK3n\":[\"Primo messaggio da inviare\"],\"Y/qryO\":[\"Nessun utente trovato corrispondente alla ricerca\"],\"YAqRpI\":[\"Registrazione dell'account \",[\"account\"],\" riuscita: \",[\"message\"]],\"YBXJ7j\":[\"IN\"],\"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:\"],\"Z7ZXbT\":[\"Approva\"],\"ZJSWfw\":[\"Messaggio mostrato alla disconnessione dal server\"],\"ZR1dJ4\":[\"Inviti\"],\"ZdWg0V\":[\"Apri nel browser\"],\"ZhRBbl\":[\"Cerca messaggi…\"],\"Zmcu3y\":[\"Filtri avanzati\"],\"ZqLD8l\":[\"A livello di server\"],\"a2/8e5\":[\"Argomento impostato dopo (min fa)\"],\"aHKcKc\":[\"Pagina precedente\"],\"aJTbXX\":[\"Password oper\"],\"aP9gNu\":[\"output troncato\"],\"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\"],\"bFDO8z\":[\"gateway online\"],\"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.\"],\"bVBC/W\":[\"Gateway connesso\"],\"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\"],\"cde3ce\":[\"Messaggio a <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\"],\"dRqrdL\":[[\"0\"],\" deve essere un numero intero.\"],\"dXqxlh\":[\"<0>⚠️ Rischio di sicurezza! Questa connessione potrebbe essere vulnerabile a intercettazioni o attacchi man-in-the-middle.\"],\"da9Q/R\":[\"Modalità canale modificate\"],\"dhJN3N\":[\"Mostra commenti\"],\"dj2xTE\":[\"Ignora notifica\"],\"dnUmOX\":[\"Nessun bot registrato su questa rete al momento.\"],\"dpCzmC\":[\"Impostazioni protezione flood\"],\"e7KzRG\":[[\"0\"],\" passaggio/i\"],\"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: 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.\"],\"fYdEvu\":[\"Cronologia workflow (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Seleziona un bot a sinistra per vederne i comandi e le azioni di gestione.\"],\"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\"],\"jUXib7\":[\"Il messaggio di risposta non è più visibile\"],\"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:\"],\"kG2fiE\":[\"definito da config\"],\"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.\"],\"ky2mw7\":[\"tramite @\",[\"0\"]],\"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\"],\"lH+ed1\":[\"In attesa del primo passaggio…\"],\"lHy8N5\":[\"Caricamento altri canali...\"],\"lasgrr\":[\"usato\"],\"lbpf14\":[\"Entra in \",[\"value\"]],\"lf3MT4\":[\"Canale da abbandonare (predefinito: corrente)\"],\"lfFsZ4\":[\"Canali\"],\"lkNdiH\":[\"Nome account\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Carica immagine\"],\"loQxaJ\":[\"Sono tornato\"],\"lvfaxv\":[\"HOME\"],\"m16xKo\":[\"Aggiungi\"],\"m8flAk\":[\"Anteprima (non ancora caricata)\"],\"mDkV0w\":[\"Avvio del workflow…\"],\"mEPxTp\":[\"<0>⚠️ Attenzione! Apri solo link da fonti attendibili. I link malevoli possono compromettere la tua sicurezza o privacy.\"],\"mHGdhG\":[\"Informazioni sul server\"],\"mHS8lb\":[\"Messaggio in #\",[\"0\"]],\"mHfd/S\":[\"Cosa stai facendo\"],\"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\"],\"nX4XLG\":[\"Azioni operatore\"],\"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\"],\"oaTtrx\":[\"Cerca bot\"],\"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\"],\"pQBYsE\":[\"Risposto in chat\"],\"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\"],\"qXgujk\":[\"Invia un'azione / emote\"],\"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\"],\" presenta le seguenti problematiche di sicurezza:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"ora è conosciuto come **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" ti ha invitato a unirti a \",[\"channel\"]],\"sW5OjU\":[\"richiesto\"],\"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\"],\"thC9Rq\":[\"Abbandona un canale\"],\"tiBsJk\":[\"ha lasciato \",[\"channelName\"]],\"tt4/UD\":[\"ha abbandonato (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Canale a cui unirsi (#nome)\"],\"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\"]],\"uMIUx8\":[\"Eliminare il bot \",[\"0\"],\"? Questo elimina logicamente la riga del database; riutilizza il nick in seguito solo dopo un /REHASH.\"],\"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\"],\"wCKe3+\":[\"Cronologia workflow\"],\"wFjjxZ\":[\"è stato espulso da \",[\"channelName\"],\" da \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nessuna eccezione di ban trovata\"],\"wPrGnM\":[\"Amministratore del canale\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Ragionamento\"],\"wbm86v\":[\"Mostra quando gli utenti entrano o escono dai canali\"],\"wdxz7K\":[\"Sorgente\"],\"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/invitationnon è annunciata). Puoi comunque chattare normalmente; questo pannello è per le reti basate su obbyircd.\"],\"xceQrO\":[\"Sono supportati solo websocket sicuri\"],\"xdtXa+\":[\"nome-canale\"],\"xeiujy\":[\"Testo\"],\"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\"]],\"zPBDzU\":[\"Annulla workflow\"],\"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..d4ee879b 100644 --- a/src/locales/it/messages.po +++ b/src/locales/it/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Portare IRC nel futuro" msgid "— open in viewer" msgstr "— apri nel visualizzatore" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(gestito da ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(sospeso)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Mostra 1 altro elemento} other {Mostra {1} altri elemen msgid "{0} and {1} are typing..." msgstr "{0} e {1} stanno scrivendo..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} è obbligatorio." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} sta scrivendo..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} deve essere un numero." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} deve essere un numero intero." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} messaggi precedenti" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} passaggio/i" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} passaggio/i in attesa di approvazione" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Filtri avanzati" msgid "Album" msgstr "Album" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Tutti" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Tutto il contenuto" @@ -297,6 +334,12 @@ msgstr "Applica filtri e aggiorna" msgid "Applying..." msgstr "Applicazione..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Approva" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Account autenticato" msgid "Auto Fallback to Single Line" msgstr "Ritorno automatico alla singola riga" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Chiusura automatica tra {secondsLeft}s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Autentica automaticamente come operatore alla connessione" @@ -359,6 +406,10 @@ msgstr "Assente" msgid "Away from keyboard" msgstr "Lontano dalla tastiera" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Messaggio di assenza" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Messaggio di assenza" msgid "Away:" msgstr "Assente:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Ban" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Il bot non ha ancora registrato alcun comando slash." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Bot" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Bot su questa rete" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Sfoglia tutti i canali del server" @@ -430,6 +499,7 @@ msgstr "Sfoglia tutti i canali del server" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Annulla connessione" msgid "Cancel reply" msgstr "Annulla risposta" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Annulla workflow" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Cambia il nome del canale (solo operatori)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Cambia il tuo nickname su questo server" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Modalità canale modificate" @@ -459,6 +537,7 @@ msgstr "Nickname cambiato" msgid "changed the topic to: {topic}" msgstr "ha cambiato il topic in: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Canale" @@ -519,6 +598,14 @@ msgstr "Proprietario del canale" msgid "Channel Settings" msgstr "Impostazioni canale" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Canale a cui unirsi (#nome)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Canale da abbandonare (predefinito: corrente)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Argomento del canale" @@ -531,6 +618,7 @@ msgstr "Il canale non apparirà nei comandi LIST o NAMES" msgid "channel-name" msgstr "nome-canale" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Canali" @@ -606,6 +694,9 @@ msgstr "Limite client (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Commenti" msgid "Comments ({commentCount})" msgstr "Commenti ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "definito da config" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Bot definito da configurazione. Modifica obbyircd.conf e usa /REHASH per cambiarne lo stato." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "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." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Soprannome predefinito" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Elimina" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Eliminare il bot {0}? Questo elimina logicamente la riga del database; riutilizza il nick in seguito solo dopo un /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Elimina canale" @@ -843,6 +948,10 @@ msgstr "Scopri" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Scopri il mondo di IRC con ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Ignora" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Ignora notifica" @@ -891,7 +1000,7 @@ msgstr "Scarica" #: src/components/layout/ChatArea.tsx msgid "Drop files to upload" -msgstr "Rilascia i file per caricarli" +msgstr "Trascina qui i file da caricare" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Filtra canali..." msgid "Filter members…" msgstr "Filtra membri…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Primo messaggio da inviare" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Profilo flood (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (ban globale)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway connesso" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway online" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Generale" @@ -1186,6 +1307,10 @@ msgstr "Immagine {0} di {1}" msgid "Image preview" msgstr "Anteprima immagine" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "IN" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1270,6 +1395,10 @@ msgstr "Unisciti a {0}" msgid "Join {value}" msgstr "Entra in {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Unisciti a un canale" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Scopri di più sulle regole personalizzate →" msgid "Learn more about profiles →" msgstr "Scopri di più sui profili →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Abbandona un canale" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Abbandona canale" @@ -1411,6 +1544,14 @@ msgstr "Gestisci le informazioni del tuo profilo e i metadati" msgid "Mark as bot - usually 'on' or empty" msgstr "Segna come bot, di solito 'on' o vuoto" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Segnati come assente" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Segnati come tornato" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Utenti max." @@ -1573,6 +1714,10 @@ msgstr "Nome rete" msgid "New DM" msgstr "Nuovo messaggio privato" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Nuovo nickname" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Immagine successiva" @@ -1617,6 +1762,10 @@ msgstr "Nessuna eccezione di ban trovata" msgid "No bans found" msgstr "Nessun ban trovato" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Nessun bot registrato su questa rete al momento." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Nessun server centrale:" @@ -1672,7 +1821,7 @@ msgstr "Nessuna anteprima media caricata." #: src/components/ui/RawLogViewer.tsx msgid "No raw IRC traffic captured yet. Try connecting or sending a message." -msgstr "Nessun traffico IRC raw ancora catturato. Prova a connetterti o a inviare un messaggio." +msgstr "Nessun traffico IRC grezzo catturato. Prova a connetterti o a inviare un messaggio." #: src/components/ui/QuickActions.tsx msgid "No results found" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - Portare IRC nel futuro" msgid "Off" msgstr "Disattivato" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "offline" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Non in linea" @@ -1754,6 +1908,10 @@ msgstr "Non in linea" msgid "on" msgstr "attivo" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "uno di:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Ops! La rete si è divisa! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Apri un messaggio privato a un utente" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Apri impostazioni configurazione canale" @@ -1828,10 +1990,23 @@ msgstr "Password oper" msgid "Oper Username" msgstr "Nome utente operatore" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Azioni operatore" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Segnalazioni di crash opzionali per migliorare l'app" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "OUT" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "output troncato" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Proprietario" @@ -1994,6 +2169,10 @@ msgstr "Messaggio di uscita" msgid "Quit the server" msgstr "Uscito dal server" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "ha eseguito" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Reagisci" @@ -2024,6 +2203,10 @@ msgstr "Motivo" msgid "Reason (optional)" msgstr "Motivo (opzionale)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Ragionamento" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Riconnetti al server" @@ -2037,6 +2220,11 @@ msgstr "Aggiorna" msgid "Register for an account" msgstr "Registra un account" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Rifiuta" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Rilassato" @@ -2077,11 +2265,27 @@ msgstr "Rinomina questo canale sul server. Tutti gli utenti vedranno il nuovo no msgid "Render markdown formatting in messages" msgstr "Mostra la formattazione Markdown nei messaggi" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Riapri il workflow che ha prodotto questo messaggio ({stepCount} passaggi)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Rispondi" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "richiesto" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Risposto in chat" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Il messaggio di risposta non è più visibile" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Riprova invio" msgid "Rules" msgstr "Regole" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Esegui" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Sicuro" @@ -2121,6 +2329,10 @@ msgstr "Salvato" msgid "Saving..." msgstr "Salvataggio..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Scorri la chat fino a questa risposta" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Vai in fondo" msgid "Search" msgstr "Cerca" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Cerca bot" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Cerca GIF per iniziare" @@ -2183,6 +2399,10 @@ msgstr "Avviso di sicurezza" msgid "Seek" msgstr "Cerca posizione" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Seleziona un bot a sinistra per vederne i comandi e le azioni di gestione." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Seleziona un canale" @@ -2195,6 +2415,10 @@ msgstr "Seleziona membro" msgid "Select or add a channel to get started." msgstr "Seleziona o aggiungi un canale per iniziare." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "auto-registrato" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Invia un GIF" msgid "Send a warning message to {username}" msgstr "Invia un messaggio di avviso a {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Invia un'azione / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Invia invito" @@ -2280,6 +2508,10 @@ msgstr "Password del server" msgid "Server-to-server communication may use unencrypted connections" msgstr "Le comunicazioni tra server potrebbero usare connessioni non cifrate" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "A livello di server" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Imposta un topic…" @@ -2376,6 +2608,10 @@ msgstr "Mostra media dall'host di file attendibile del tuo server. Nessuna richi msgid "Signed On" msgstr "Connesso dal" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Comandi slash" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Software:" @@ -2392,10 +2628,18 @@ msgstr "Ordina per utenti" msgid "SoundCloud player" msgstr "Player SoundCloud" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Sorgente" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Avvia messaggio privato" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Avvio del workflow…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Messaggi di stato" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Stop" @@ -2419,10 +2664,18 @@ msgstr "Rigido" msgid "Strict - More aggressive protection" msgstr "Rigoroso – Protezione più aggressiva" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Sospendi" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Sistema" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Testo" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Canali testuali" @@ -2447,6 +2700,14 @@ msgstr "L'argomento visualizzato per questo canale. Tutti gli utenti possono ved msgid "The user will receive an invitation to join {channelName}." msgstr "L'utente riceverà un invito per unirsi a {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Il workflow che ha prodotto questo messaggio non è più nello stato" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Questo comando non accetta parametri." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Questo campo è obbligatorio" @@ -2510,6 +2771,10 @@ msgstr "Attiva/Disattiva notifiche" msgid "Toggle search" msgstr "Attiva/Disattiva ricerca" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Strumento" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Argomento impostato dopo (min fa)" @@ -2527,6 +2792,10 @@ msgstr "Argomento:" msgid "Total: {0}" msgstr "Totale: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Trasporto" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Fonti attendibili" @@ -2561,6 +2830,10 @@ msgstr "Rimuovi fissaggio chat privata" msgid "Unpin this private message conversation" msgstr "Rimuovi il fissaggio di questa conversazione privata" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Riattiva" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Carica" @@ -2687,6 +2960,11 @@ msgstr "Molto rilassato" msgid "Very Strict" msgstr "Molto rigido" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "tramite @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2730,6 +3008,10 @@ msgstr "Utente con diritto di parola" msgid "Volume" msgstr "Volume" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "In attesa del primo passaggio…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "È stato espulso dal canale" msgid "We don't store your IRC communications on our servers" msgstr "Non archiviamo le tue comunicazioni IRC sui nostri server" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Benvenuto su {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Cosa stai facendo?" msgid "What this means:" msgstr "Cosa significa:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Cosa stai facendo" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "A cosa stai pensando?" @@ -2780,6 +3070,10 @@ msgstr "A cosa stai pensando?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Sussurra a un utente nel contesto del canale corrente" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Ampio – Portata di protezione estesa" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Cronologia workflow" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Cronologia workflow ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "In corso…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/ja/messages.mjs b/src/locales/ja/messages.mjs index 14058ddb..50d3fdfc 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>リスク: 機密情報(メッセージ、プライベート会話、認証情報)が、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\"],\" へメッセージ\"],\"chQsxg\":[\"フォーマット済み出力をコピー\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\" へようこそ!\"],\"cnGeoo\":[\"削除\"],\"coPLXT\":[\"IRC通信はサーバーに保存されません\"],\"crYH/6\":[\"SoundCloudプレーヤー\"],\"d3sis4\":[\"サーバーを追加\"],\"d9aN5k\":[[\"username\"],\" をチャンネルから削除する\"],\"dEgA5A\":[\"キャンセル\"],\"dGi1We\":[\"このプライベートメッセージの会話のピン留めを解除する\"],\"dJVuyC\":[[\"channelName\"],\" を退出しました (\",[\"reason\"],\")\"],\"dMtLDE\":[\"宛て\"],\"dXqxlh\":[\"<0>⚠️ セキュリティリスク! この接続は傍受や中間者攻撃に対して脆弱な可能性があります。\"],\"da9Q/R\":[\"チャンネルモードを変更しました\"],\"dhJN3N\":[\"コメントを表示\"],\"dj2xTE\":[\"通知を閉じる\"],\"dpCzmC\":[\"フラッド保護設定\"],\"e9dQpT\":[\"このリンクを新しいタブで開きますか?\"],\"ePK91l\":[\"編集\"],\"eYBDuB\":[\"画像をアップロードするか、動的サイズ変換のための \",[\"size\"],\" 置換を含むURLを入力してください\"],\"edBbee\":[[\"username\"],\" をホストマスクでBANする(同じIP/ホストからの再参加を防止)\"],\"ekfzWq\":[\"ユーザー設定\"],\"elPDWs\":[\"IRCクライアントの使い心地をカスタマイズする\"],\"eu2osY\":[\"<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>⚠️ 注意! 信頼できる送信元のリンクのみ開いてください。悪意のあるリンクはセキュリティやプライバシーを侵害する恐れがあります。\"],\"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\"],\" への接続には次のセキュリティ上の懸念事項があります:\"],\"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/invitationケイパビリティが告知されていません)。通常通りチャットは利用できます。このパネルは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\":[\"離席時に表示するデフォルトメッセージ\"],\"+fRR7i\":[\"停止\"],\"+mVPqU\":[\"メッセージ内のMarkdown書式を表示する\"],\"+vqCJH\":[\"認証用のアカウントユーザー名\"],\"+yPBXI\":[\"ファイルを選択\"],\"+zy2Nq\":[\"種類\"],\"/09cao\":[\"リンクセキュリティが低い(レベル \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"自分を戻ったと設定する\"],\"/3BQ4J\":[\"チャンネル外のユーザーはメッセージを送信できません\"],\"/4C8U0\":[\"すべてコピー\"],\"/6BzZF\":[\"メンバーリストを切り替え\"],\"/AkXyp\":[\"確認しますか?\"],\"/TNOPk\":[\"ユーザーは離席中です\"],\"/XQgft\":[\"探す\"],\"/cF7Rs\":[\"音量\"],\"/dqduX\":[\"次のページ\"],\"/fc3q4\":[\"すべてのコンテンツ\"],\"/kISDh\":[\"通知音を有効にする\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"音声\"],\"/rfkZe\":[\"メンションやメッセージに対してサウンドを再生する\"],\"/xQ19T\":[\"このネットワークのボット\"],\"0/0ZGA\":[\"チャンネル名マスク\"],\"0D6j7U\":[\"カスタムルールについて詳しく →\"],\"0XsHcR\":[\"ユーザーをキック\"],\"0ZpE//\":[\"ユーザー数順で並び替え\"],\"0bEPwz\":[\"離席中に設定\"],\"0dGkPt\":[\"チャンネルリストを展開\"],\"0gS7M5\":[\"表示名\"],\"0kS+M8\":[\"サンプルNET\"],\"0rgoY7\":[\"自分で選んだサーバーにのみ接続します\"],\"0wdd7X\":[\"参加\"],\"0wkVYx\":[\"プライベートメッセージ\"],\"111uHX\":[\"リンクプレビュー\"],\"196EG4\":[\"プライベートチャットを削除\"],\"1C/fOn\":[\"ボットはまだスラッシュコマンドを登録していません。\"],\"1DSr1i\":[\"アカウントを登録する\"],\"1O/24y\":[\"チャンネルリストを切り替え\"],\"1QfxQT\":[\"閉じる\"],\"1VPJJ2\":[\"外部リンクの警告\"],\"1ZC/dv\":[\"未読のメンションやメッセージはありません\"],\"1pO1zi\":[\"サーバー名は必須です\"],\"1t/NnN\":[\"拒否\"],\"1uwfzQ\":[\"チャンネルトピックを表示\"],\"268g7c\":[\"表示名を入力\"],\"2F9+AZ\":[\"生のIRCトラフィックはまだキャプチャされていません。接続するかメッセージを送信してみてください。\"],\"2FOFq1\":[\"ネットワーク上のサーバーオペレーターがメッセージを閲覧できる可能性があります\"],\"2FYpfJ\":[\"詳細\"],\"2HF1Y2\":[[\"inviter\"],\" が \",[\"target\"],\" を \",[\"channel\"],\" に招待しました\"],\"2I70QL\":[\"ユーザープロフィール情報を表示する\"],\"2QYdmE\":[\"ユーザー:\"],\"2QpEjG\":[\"退出しました\"],\"2YE223\":[\"#\",[\"0\"],\" へメッセージ(Enterで改行、Shift+Enterで送信)\"],\"2bimFY\":[\"サーバーパスワードを使用する\"],\"2iTmdZ\":[\"ローカルストレージ:\"],\"2odkwe\":[\"厳格 — より積極的な保護\"],\"2uDhbA\":[\"招待するユーザー名を入力\"],\"2xXP/g\":[\"チャンネルに参加する\"],\"2ygf/L\":[\"← 戻る\"],\"2zEgxj\":[\"GIFを検索...\"],\"3JjdaA\":[\"実行\"],\"3NJ4MW\":[\"このメッセージを生成したワークフローを再度開く(\",[\"stepCount\"],\" ステップ)\"],\"3RdPhl\":[\"チャンネル名を変更\"],\"3THokf\":[\"Voiceユーザー\"],\"3TSz9S\":[\"最小化\"],\"3et0TM\":[\"チャットをこの応答までスクロール\"],\"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行入力に切り替える\"],\"4uKgKr\":[\"送信\"],\"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\":[\"プライバシーに関するご質問はこちら:\"],\"9q17ZR\":[[\"0\"],\" は必須です。\"],\"9qIYMn\":[\"新しいニックネーム\"],\"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\":[\"保存されていない変更があります。保存せずに閉じてもよいですか?\"],\"BIDT9R\":[\"ボット\"],\"BIf9fi\":[\"ステータスメッセージ\"],\"BPm98R\":[\"サーバーが選択されていません。まずサイドバーからサーバーを選択してください。招待リンクはサーバーごとに管理されます。\"],\"BZz3md\":[\"個人ウェブサイト\"],\"Bgm/H7\":[\"複数行のテキスト入力を許可する\"],\"BiQIl1\":[\"このプライベートメッセージの会話をピン留めする\"],\"BlNZZ2\":[\"クリックしてメッセージに移動\"],\"Bowq3c\":[\"オペレーターのみがチャンネルトピックを変更できます\"],\"Btozzp\":[\"この画像の有効期限が切れています\"],\"Bycfjm\":[\"合計:\",[\"0\"]],\"C6IBQc\":[\"JSON全体をコピー\"],\"C9L9wL\":[\"データ収集\"],\"CDq4wC\":[\"ユーザーをモデレート\"],\"CHVRxG\":[\"@\",[\"0\"],\" へメッセージ(Shift+Enterで改行)\"],\"CN9zdR\":[\"Oper名とパスワードは必須です\"],\"CW3sYa\":[\"リアクション \",[\"emoji\"],\" を追加\"],\"CaAkqd\":[\"退出を表示\"],\"CaQ1Gb\":[\"設定で定義されたボットです。状態を変更するには obbyircd.conf を編集して /REHASH を実行してください。\"],\"CbvaYj\":[\"ニックネームでBAN\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"チャンネルを選択\"],\"CsekCi\":[\"通常\"],\"D+NlUC\":[\"システム\"],\"D28t6+\":[\"参加して退出しました\"],\"DB8zMK\":[\"適用\"],\"DBcWHr\":[\"カスタム通知音ファイル\"],\"DSHF2K\":[\"このメッセージを生成したワークフローはもう状態にありません\"],\"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\":[\"サーバーを検索...\"],\"Eu7YKa\":[\"自己登録\"],\"F0OKMc\":[\"サーバーを編集\"],\"F6Int2\":[\"ハイライトを有効にする\"],\"FDoLyE\":[\"最大ユーザー数\"],\"FUU/hZ\":[\"チャットで読み込む外部メディアの量を制御します。\"],\"Fdp03t\":[\"オン\"],\"FfPWR0\":[\"モーダル\"],\"FjkaiT\":[\"ズームアウト\"],\"FlqOE9\":[\"これが意味すること:\"],\"FolHNl\":[\"アカウントと認証を管理する\"],\"Fp2Dif\":[\"サーバーを退出しました\"],\"G5KmCc\":[\"GZ-Line(グローバルZ-Line)\"],\"GDs0lz\":[\"<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\"],\" が入力中...\"],\"IcHxhR\":[\"オフライン\"],\"IgrLD/\":[\"一時停止\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"返信\"],\"IoHMnl\":[\"最大値は \",[\"0\"],\" です\"],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"接続中...\"],\"J5T9NW\":[\"ユーザー情報\"],\"J8Y5+z\":[\"おっと!ネットワーク分割が発生しました!⚠️\"],\"JBHkBA\":[\"チャンネルを退出しました\"],\"JCwL0Q\":[\"理由を入力(任意)\"],\"JFciKP\":[\"切り替え\"],\"JMXMCX\":[\"離席メッセージ\"],\"JXGkhG\":[\"チャンネル名を変更する(オペレーターのみ)\"],\"JYiL1b\":[\"次のいずれか:\"],\"JcD7qf\":[\"その他のアクション\"],\"JdkA+c\":[\"シークレット (+s)\"],\"Jmu12l\":[\"サーバーチャンネル\"],\"JvQ++s\":[\"Markdownを有効にする\"],\"K2jwh/\":[\"WHOISデータがありません\"],\"K4vEhk\":[\"(停止中)\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"メッセージを削除\"],\"KKBlUU\":[\"埋め込み\"],\"KM0pLb\":[\"チャンネルへようこそ!\"],\"KR6W2h\":[\"無視を解除\"],\"KV+Bi1\":[\"招待制 (+i)\"],\"KdCtwE\":[\"カウンターをリセットするまでフラッドアクティビティを監視する秒数\"],\"Kkezga\":[\"サーバーパスワード\"],\"KsiQ/8\":[\"ユーザーはチャンネルに参加するために招待が必要です\"],\"KtADxr\":[\"が実行\"],\"L+gB/D\":[\"チャンネル情報\"],\"LC1a7n\":[\"IRCサーバーは、サーバー間リンクのセキュリティレベルが低いと報告しています。これは、ネットワーク内のIRCサーバー間でメッセージが中継される際に、適切に暗号化されていないか、SSL/TLS証明書が正しく検証されない可能性があることを意味します。\"],\"LN3RO2\":[[\"0\"],\" ステップが承認待ちです\"],\"LNfLR5\":[\"キックを表示\"],\"LQb0W/\":[\"すべてのイベントを表示\"],\"LU7/yA\":[\"UI上で表示するための別名です。スペース、絵文字、特殊文字を含めることができます。実際のチャンネル名(\",[\"channelName\"],\")は引き続きIRCコマンドで使用されます。\"],\"LUb9O7\":[\"有効なサーバーポートが必要です\"],\"LV4fT6\":[\"説明(任意、例:「Q3ベータテスター」)\"],\"LYzbQ2\":[\"ツール\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"プライバシーポリシー\"],\"LcuSDR\":[\"プロフィール情報とメタデータを管理する\"],\"LqLS9B\":[\"ニックネーム変更を表示\"],\"LsDQt2\":[\"チャンネル設定\"],\"LtI9AS\":[\"オーナー\"],\"LuNhhL\":[\"このメッセージにリアクションしました\"],\"M/AZNG\":[\"アバター画像のURL\"],\"M/WIer\":[\"メッセージを送信\"],\"M45wtf\":[\"このコマンドはパラメータを受け取りません。\"],\"M8er/5\":[\"名前:\"],\"MHk+7g\":[\"前の画像\"],\"MRorGe\":[\"DMを送る\"],\"MVbSGP\":[\"時間ウィンドウ(秒)\"],\"MkpcsT\":[\"メッセージと設定はデバイス上にローカルで保存されます\"],\"N/hDSy\":[\"ボットとしてマーク — 通常は「on」または空欄\"],\"N40H+G\":[\"すべて\"],\"N7TQbE\":[[\"channelName\"],\" にユーザーを招待\"],\"NCca/o\":[\"デフォルトニックネームを入力...\"],\"NQN2HS\":[\"停止を解除\"],\"Nqs6B9\":[\"すべての外部メディアを表示します。URLが不明なサーバーへのリクエストを引き起こす場合があります。\"],\"Nt+9O7\":[\"生のTCPの代わりにWebSocketを使用する\"],\"NxIHzc\":[\"ユーザーを切断\"],\"O+HhhG\":[\"現在のチャンネルのコンテキストでユーザーにささやく\"],\"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\"]],\"RlCInP\":[\"スラッシュコマンド\"],\"RySp6q\":[\"コメントを非表示\"],\"RzfkXn\":[\"このサーバーでのニックネームを変更する\"],\"SPKQTd\":[\"ニックネームは必須です\"],\"SPVjfj\":[\"空欄の場合は「理由なし」がデフォルトになります\"],\"SQKPvQ\":[\"ユーザーを招待\"],\"SkZcl+\":[\"定義済みのフラッド保護プロファイルを選択してください。これらのプロファイルは、さまざまなユースケースに対してバランスの取れた保護設定を提供します。\"],\"Slr+3C\":[\"最小ユーザー数\"],\"Spnlre\":[[\"target\"],\" を \",[\"channel\"],\" に招待しました\"],\"T/ckN5\":[\"ビューアで開く\"],\"T91vKp\":[\"再生\"],\"TImSWn\":[\"(ObsidianIRC が処理)\"],\"TRDppN\":[\"Webhook\"],\"TV2Wdu\":[\"データの取り扱いとプライバシー保護について詳しく見る。\"],\"TgFpwD\":[\"適用中...\"],\"TkzSFB\":[\"変更なし\"],\"TtserG\":[\"本名を入力\"],\"Ttz9J1\":[\"パスワードを入力...\"],\"Tz0i8g\":[\"設定\"],\"U3pytU\":[\"管理者\"],\"U7yg75\":[\"自分を離席中に設定する\"],\"UDb2YD\":[\"リアクション\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"まだ招待リンクを作成していません。上のフォームから最初の招待リンクを作成してください。\"],\"UGT5vp\":[\"設定を保存\"],\"UV5hLB\":[\"BANが見つかりません\"],\"Uaj3Nd\":[\"ステータスメッセージ\"],\"Ue3uny\":[\"デフォルト(プロファイルなし)\"],\"UkARhe\":[\"通常 — 標準的な保護\"],\"Umn7Cj\":[\"まだコメントはありません。最初のコメントを投稿しましょう!\"],\"UqtiKk\":[[\"secondsLeft\"],\"秒後に自動的に閉じます\"],\"UrEy4W\":[\"ユーザーへのプライベートメッセージを開く\"],\"UtUIRh\":[[\"0\"],\" 件の古いメッセージ\"],\"UwzP+U\":[\"セキュア接続\"],\"V0/A4O\":[\"チャンネルオーナー\"],\"V2dwib\":[[\"0\"],\" は数値である必要があります。\"],\"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\":[\"プライベートチャットのピン留めを解除\"],\"XklovM\":[\"処理中…\"],\"Xm/s+u\":[\"表示\"],\"Xp2n93\":[\"サーバーの信頼済みファイルホストからのメディアを表示します。外部サービスへのリクエストは発生しません。\"],\"XvjC4F\":[\"保存中...\"],\"Y+tK3n\":[\"最初に送信するメッセージ\"],\"Y/qryO\":[\"検索に一致するユーザーが見つかりません\"],\"YAqRpI\":[[\"account\"],\" のアカウント登録に成功しました: \",[\"message\"]],\"YBXJ7j\":[\"受信\"],\"YEfzvP\":[\"トピック保護 (+t)\"],\"YQOn6a\":[\"メンバーリストを折りたたむ\"],\"YRCoE9\":[\"チャンネルオペレーター\"],\"YURQaF\":[\"プロフィールを表示\"],\"YdBSvr\":[\"メディア表示と外部コンテンツを制御する\"],\"Yj6U3V\":[\"中央サーバーなし:\"],\"YjvpGx\":[\"代名詞\"],\"YqH4l4\":[\"キーなし\"],\"YyUPpV\":[\"アカウント:\"],\"Z7ZXbT\":[\"承認\"],\"ZJSWfw\":[\"サーバーから切断したときに表示されるメッセージ\"],\"ZR1dJ4\":[\"招待\"],\"ZdWg0V\":[\"ブラウザで開く\"],\"ZhRBbl\":[\"メッセージを検索…\"],\"Zmcu3y\":[\"詳細フィルター\"],\"ZqLD8l\":[\"サーバー全体\"],\"a2/8e5\":[\"トピック設定日時(以降、分前)\"],\"aHKcKc\":[\"前のページ\"],\"aJTbXX\":[\"Operパスワード\"],\"aP9gNu\":[\"出力は切り詰められました\"],\"aQryQv\":[\"パターンはすでに存在します\"],\"aW9pLN\":[\"チャンネルに参加できる最大ユーザー数。制限なしの場合は空欄にしてください。\"],\"ah4fmZ\":[\"YouTube、Vimeo、SoundCloudなどの既知のサービスのプレビューも表示します。\"],\"aifXak\":[\"このチャンネルにはメディアがありません\"],\"ap2zBz\":[\"緩め\"],\"az8lvo\":[\"オフ\"],\"azXSNo\":[\"メンバーリストを展開\"],\"azdliB\":[\"アカウントにログイン\"],\"b26wlF\":[\"彼女/彼女の\"],\"bD/+Ei\":[\"厳格\"],\"bFDO8z\":[\"gateway オンライン\"],\"bQ6BJn\":[\"詳細なフラッド保護ルールを設定します。各ルールは、監視するアクティビティの種類と、しきい値を超えた場合に実行するアクションを指定します。\"],\"bVBC/W\":[\"Gateway 接続済み\"],\"beV7+y\":[\"ユーザーは \",[\"channelName\"],\" への参加招待を受け取ります。\"],\"bk84cH\":[\"離席メッセージ\"],\"bkHdLj\":[\"IRCサーバーを追加\"],\"bmQLn5\":[\"ルールを追加\"],\"bv4cFj\":[\"トランスポート\"],\"bwRvnp\":[\"アクション\"],\"c8+EVZ\":[\"認証済みアカウント\"],\"cGYUlD\":[\"メディアプレビューは読み込まれません。\"],\"cLF98o\":[\"コメントを表示 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"利用可能なユーザーがいません\"],\"cSgpoS\":[\"プライベートチャットをピン留め\"],\"cde3ce\":[\"<0>\",[\"0\"],\" へメッセージ\"],\"chQsxg\":[\"フォーマット済み出力をコピー\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\" へようこそ!\"],\"cnGeoo\":[\"削除\"],\"coPLXT\":[\"IRC通信はサーバーに保存されません\"],\"crYH/6\":[\"SoundCloudプレーヤー\"],\"d3sis4\":[\"サーバーを追加\"],\"d9aN5k\":[[\"username\"],\" をチャンネルから削除する\"],\"dEgA5A\":[\"キャンセル\"],\"dGi1We\":[\"このプライベートメッセージの会話のピン留めを解除する\"],\"dJVuyC\":[[\"channelName\"],\" を退出しました (\",[\"reason\"],\")\"],\"dMtLDE\":[\"宛て\"],\"dRqrdL\":[[\"0\"],\" は整数である必要があります。\"],\"dXqxlh\":[\"<0>⚠️ セキュリティリスク! この接続は傍受や中間者攻撃に対して脆弱な可能性があります。\"],\"da9Q/R\":[\"チャンネルモードを変更しました\"],\"dhJN3N\":[\"コメントを表示\"],\"dj2xTE\":[\"通知を閉じる\"],\"dnUmOX\":[\"このネットワークにはまだボットが登録されていません。\"],\"dpCzmC\":[\"フラッド保護設定\"],\"e7KzRG\":[[\"0\"],\" ステップ\"],\"e9dQpT\":[\"このリンクを新しいタブで開きますか?\"],\"ePK91l\":[\"編集\"],\"eYBDuB\":[\"画像をアップロードするか、動的サイズ変換のための \",[\"size\"],\" 置換を含むURLを入力してください\"],\"edBbee\":[[\"username\"],\" をホストマスクでBANする(同じIP/ホストからの再参加を防止)\"],\"ekfzWq\":[\"ユーザー設定\"],\"elPDWs\":[\"IRCクライアントの使い心地をカスタマイズする\"],\"eu2osY\":[\"<0>💡 推奨事項: このサーバーを信頼し、リスクを理解している場合のみ続行してください。この接続で機密情報やパスワードを共有しないようにしてください。\"],\"euEhbr\":[\"クリックして \",[\"channel\"],\" に参加\"],\"ez3vLd\":[\"複数行入力を有効にする\"],\"f0J5Ki\":[\"サーバー間通信に暗号化されていない接続が使用される可能性があります\"],\"f9BHJk\":[\"ユーザーに警告\"],\"fDOLLd\":[\"チャンネルが見つかりません。\"],\"fYdEvu\":[\"ワークフロー履歴(\",[\"0\"],\")\"],\"ffzDkB\":[\"匿名分析:\"],\"fq1GF9\":[\"ユーザーがサーバーから切断したときに表示\"],\"gEF57C\":[\"このサーバーは1種類の接続タイプのみサポートしています\"],\"gJuLUI\":[\"無視リスト\"],\"gNzMrk\":[\"現在のアバター\"],\"gjPWyO\":[\"ニックネームを入力...\"],\"gz6UQ3\":[\"最大化\"],\"h6razj\":[\"チャンネル名マスクを除外\"],\"hG6jnw\":[\"トピックが設定されていません\"],\"hG89Ed\":[\"画像\"],\"hYgDIe\":[\"作成\"],\"hZ6znB\":[\"ポート\"],\"ha+Bz5\":[\"例:100:1440\"],\"hctjqj\":[\"コマンドと管理アクションを表示するには、左側のボットを選択してください。\"],\"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\":[\"アバターをアップロード\"],\"jUXib7\":[\"応答メッセージは表示されていません\"],\"jW5Uwh\":[\"外部メディアの読み込み範囲を制御します。オフ/安全/信頼できるソース/すべてのコンテンツ。\"],\"jXzms5\":[\"添付オプション\"],\"jZlrte\":[\"カラー\"],\"jfC/xh\":[\"連絡先\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"古いメッセージを読み込む\"],\"k3ID0F\":[\"メンバーをフィルター…\"],\"k65gsE\":[\"詳細を見る\"],\"k7Zgob\":[\"接続をキャンセル\"],\"kAVx5h\":[\"招待が見つかりません\"],\"kCLEPU\":[\"接続先\"],\"kF5LKb\":[\"無視パターン:\"],\"kG2fiE\":[\"設定で定義済み\"],\"kGeOx/\":[[\"0\"],\" に参加\"],\"kITKr8\":[\"チャンネルモードを読み込み中...\"],\"kPpPsw\":[\"あなたはIRC Operatorです\"],\"kWJmRL\":[\"あなた\"],\"kfcRb0\":[\"アバター\"],\"kjMqSj\":[\"JSONをコピー\"],\"krViRy\":[\"JSONとしてコピーするにはクリック\"],\"ks71ra\":[\"例外\"],\"kw4lRv\":[\"チャンネルハーフオペレーター\"],\"kxgIRq\":[\"チャンネルを選択または追加して始めましょう。\"],\"ky2mw7\":[\"@\",[\"0\"],\" 経由\"],\"ky6dWe\":[\"アバタープレビュー\"],\"l+GxCv\":[\"チャンネルを読み込み中...\"],\"l+IUVW\":[[\"account\"],\" のアカウント確認に成功しました: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[[\"reconnectCount\"],\"回 再接続した\"]}]],\"l1l8sj\":[[\"0\"],\"日前\"],\"l5NhnV\":[\"#チャンネル(任意)\"],\"l5jmzx\":[[\"0\"],\" と \",[\"1\"],\" が入力中...\"],\"lCF0wC\":[\"更新\"],\"lH+ed1\":[\"最初のステップを待っています…\"],\"lHy8N5\":[\"さらにチャンネルを読み込み中...\"],\"lasgrr\":[\"使用済み\"],\"lbpf14\":[[\"value\"],\"に参加\"],\"lf3MT4\":[\"退出するチャンネル(既定では現在のチャンネル)\"],\"lfFsZ4\":[\"チャンネル\"],\"lkNdiH\":[\"アカウント名\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"画像をアップロード\"],\"loQxaJ\":[\"戻りました\"],\"lvfaxv\":[\"ホーム\"],\"m16xKo\":[\"追加\"],\"m8flAk\":[\"プレビュー(未アップロード)\"],\"mDkV0w\":[\"ワークフローを開始しています…\"],\"mEPxTp\":[\"<0>⚠️ 注意! 信頼できる送信元のリンクのみ開いてください。悪意のあるリンクはセキュリティやプライバシーを侵害する恐れがあります。\"],\"mHGdhG\":[\"サーバー情報\"],\"mHS8lb\":[\"#\",[\"0\"],\" へメッセージ\"],\"mHfd/S\":[\"あなたの行動\"],\"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\":[\"ピン留めを解除\"],\"nX4XLG\":[\"Operator アクション\"],\"nkC032\":[\"フラッドプロファイルなし\"],\"o69z4d\":[[\"username\"],\" に警告メッセージを送る\"],\"o9ylQi\":[\"GIFを検索して始めましょう\"],\"oFGkER\":[\"サーバー通知\"],\"oOi11l\":[\"最下部にスクロール\"],\"oPYIL5\":[\"ネットワーク\"],\"oQEzQR\":[\"新しいDM\"],\"oXOSPE\":[\"オンライン\"],\"oaTtrx\":[\"ボットを検索\"],\"oal760\":[\"サーバーリンクへの中間者攻撃が可能な状態です\"],\"oeqmmJ\":[\"信頼できるソース\"],\"optX0N\":[[\"0\"],\"時間前\"],\"ovBPCi\":[\"デフォルト\"],\"p0Z69r\":[\"パターンを空にすることはできません\"],\"p1KgtK\":[\"音声の読み込みに失敗しました\"],\"p59pEv\":[\"詳細情報\"],\"p7sRI6\":[\"入力中であることを他のユーザーに知らせる\"],\"pBm1od\":[\"シークレットチャンネル\"],\"pNmiXx\":[\"すべてのサーバーで使用するデフォルトのニックネーム\"],\"pQBYsE\":[\"チャットで応答しました\"],\"pUUo9G\":[\"ホスト名:\"],\"pVGPmz\":[\"アカウントパスワード\"],\"peNE68\":[\"永続的\"],\"plhHQt\":[\"データなし\"],\"pm6+q5\":[\"セキュリティ警告\"],\"pn5qSs\":[\"追加情報\"],\"q0cR4S\":[\"は現在 **\",[\"newNick\"],\"** として知られています\"],\"qFcunY\":[\"LIST または NAMES コマンドにチャンネルが表示されません\"],\"qLpTm/\":[\"リアクション \",[\"emoji\"],\" を削除\"],\"qVkGWK\":[\"ピン留め\"],\"qXgujk\":[\"アクション / エモートを送信する\"],\"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\"],\" への接続には次のセキュリティ上の懸念事項があります:\"],\"sGH11W\":[\"サーバー\"],\"sHI1H+\":[\"は現在 **\",[\"newNick\"],\"** として知られています\"],\"sJyV04\":[[\"inviter\"],\" があなたを \",[\"channel\"],\" に招待しました\"],\"sW5OjU\":[\"必須\"],\"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\":[\"保存\"],\"thC9Rq\":[\"チャンネルから退出する\"],\"tiBsJk\":[[\"channelName\"],\" を退出しました\"],\"tt4/UD\":[\"退出しました (\",[\"reason\"],\")\"],\"tu2JEr\":[\"参加するチャンネル (#name)\"],\"u0TcnO\":[\"ニックネーム {nick} は既に使用中です。{newNick} で再試行します\"],\"u0a8B4\":[\"管理者アクセスのためにIRC Operatorとして認証する\"],\"u0rWFU\":[\"作成日時(以降、分前)\"],\"u72w3t\":[\"無視するユーザーとパターン\"],\"u7jc2L\":[\"退出しました\"],\"uAQUqI\":[\"ステータス\"],\"uB85T3\":[\"保存失敗:\",[\"msg\"]],\"uMIUx8\":[\"ボット \",[\"0\"],\" を削除しますか? これはデータベース行をソフト削除します。ニックは /REHASH の後にのみ再利用してください。\"],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRCサーバー:\"],\"ukyW4o\":[\"あなたの招待リンク\"],\"usSSr/\":[\"ズームレベル\"],\"v7uvcf\":[\"ソフトウェア:\"],\"vE8kb+\":[\"Shift+Enterで改行(Enterで送信)\"],\"vERlcd\":[\"プロフィール\"],\"vK0RL8\":[\"トピックなし\"],\"vSJd18\":[\"動画\"],\"vXIe7J\":[\"言語\"],\"vaHYxN\":[\"本名\"],\"vhjbKr\":[\"離席中\"],\"w4NYox\":[[\"title\"],\" クライアント\"],\"w8xQRx\":[\"無効な値\"],\"wCKe3+\":[\"ワークフロー履歴\"],\"wFjjxZ\":[[\"username\"],\" によって \",[\"channelName\"],\" からキックされました (\",[\"reason\"],\")\"],\"wGjaGl\":[\"BAN例外が見つかりません\"],\"wPrGnM\":[\"チャンネル管理者\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"推論\"],\"wbm86v\":[\"ユーザーがチャンネルに参加・退出したときに表示する\"],\"wdxz7K\":[\"ソース\"],\"whqZ9r\":[\"ハイライトする追加の単語またはフレーズ\"],\"wm7RV4\":[\"通知音\"],\"wz/Yoq\":[\"サーバー間で中継される際にメッセージが傍受される可能性があります\"],\"x3+y8b\":[\"このリンクから登録した人数\"],\"xCJdfg\":[\"クリア\"],\"xOTzt5\":[\"たった今\"],\"xUHRTR\":[\"接続時に自動的にOperatorとして認証する\"],\"xWHwwQ\":[\"BAN一覧\"],\"xYilR2\":[\"メディア\"],\"xbi8D6\":[\"このサーバーは招待リンクに対応していません(<0>obby.world/invitationケイパビリティが告知されていません)。通常通りチャットは利用できます。このパネルはobbyircdベースのネットワーク用です。\"],\"xceQrO\":[\"安全なWebSocketのみサポートされています\"],\"xdtXa+\":[\"チャンネル名\"],\"xeiujy\":[\"テキスト\"],\"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\"]],\"zPBDzU\":[\"ワークフローをキャンセル\"],\"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..cc20a789 100644 --- a/src/locales/ja/messages.po +++ b/src/locales/ja/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - IRCを未来へ" msgid "— open in viewer" msgstr "— ビューアで開く" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(ObsidianIRC が処理)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +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); @@ -41,16 +49,41 @@ msgstr "{0, plural, other {さらに {1} 件表示}}" msgid "{0} and {1} are typing..." msgstr "{0} と {1} が入力中..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} は必須です。" + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} が入力中..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} は数値である必要があります。" + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} は整数である必要があります。" + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} 件の古いメッセージ" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} ステップ" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} ステップが承認待ちです" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "詳細フィルター" msgid "Album" msgstr "アルバム" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "すべて" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "すべてのコンテンツ" @@ -297,6 +334,12 @@ msgstr "フィルターを適用して更新" msgid "Applying..." msgstr "適用中..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "承認" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "認証済みアカウント" msgid "Auto Fallback to Single Line" msgstr "自動的に1行入力に切り替え" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "{secondsLeft}秒後に自動的に閉じます" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "接続時に自動的にOperatorとして認証する" @@ -359,6 +406,10 @@ msgstr "離席中" msgid "Away from keyboard" msgstr "席を外しています" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "離席メッセージ" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "離席メッセージ" msgid "Away:" msgstr "退席中:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "BAN一覧" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "ボット" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "ボットはまだスラッシュコマンドを登録していません。" + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "ボット" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "このネットワークのボット" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "サーバー上のすべてのチャンネルを一覧表示" @@ -430,6 +499,7 @@ msgstr "サーバー上のすべてのチャンネルを一覧表示" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "接続をキャンセル" msgid "Cancel reply" msgstr "返信をキャンセル" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "ワークフローをキャンセル" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "チャンネル名を変更する(オペレーターのみ)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "このサーバーでのニックネームを変更する" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "チャンネルモードを変更しました" @@ -459,6 +537,7 @@ msgstr "ニックネームを変更しました" msgid "changed the topic to: {topic}" msgstr "トピックを変更しました: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "チャンネル" @@ -519,6 +598,14 @@ msgstr "チャンネルオーナー" msgid "Channel Settings" msgstr "チャンネル設定" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "参加するチャンネル (#name)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "退出するチャンネル(既定では現在のチャンネル)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "チャンネルトピック" @@ -531,6 +618,7 @@ msgstr "LIST または NAMES コマンドにチャンネルが表示されませ msgid "channel-name" msgstr "チャンネル名" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "チャンネル" @@ -606,6 +694,9 @@ msgstr "クライアント制限 (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "コメント" msgid "Comments ({commentCount})" msgstr "コメント ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "設定で定義済み" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "設定で定義されたボットです。状態を変更するには obbyircd.conf を編集して /REHASH を実行してください。" + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "詳細なフラッド保護ルールを設定します。各ルールは、監視するアクティビティの種類と、しきい値を超えた場合に実行するアクションを指定します。" @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "デフォルトニックネーム" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "削除" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "ボット {0} を削除しますか? これはデータベース行をソフト削除します。ニックは /REHASH の後にのみ再利用してください。" + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "チャンネルを削除" @@ -843,6 +948,10 @@ msgstr "探す" msgid "Discover the world of IRC with ObsidianIRC" msgstr "ObsidianIRCでIRCの世界を探索しよう" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "閉じる" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "通知を閉じる" @@ -891,7 +1000,7 @@ msgstr "ダウンロード" #: src/components/layout/ChatArea.tsx msgid "Drop files to upload" -msgstr "ファイルをドロップしてアップロード" +msgstr "アップロードするファイルをドロップ" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "チャンネルをフィルター..." msgid "Filter members…" msgstr "メンバーをフィルター…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "最初に送信するメッセージ" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "フラッドプロファイル (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line(グローバルBAN)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway 接続済み" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway オンライン" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "一般" @@ -1186,6 +1307,10 @@ msgstr "{1}枚中{0}枚目の画像" msgid "Image preview" msgstr "画像プレビュー" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "受信" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "情報:" @@ -1270,6 +1395,10 @@ msgstr "{0} に参加" msgid "Join {value}" msgstr "{value}に参加" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "チャンネルに参加する" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "カスタムルールについて詳しく →" msgid "Learn more about profiles →" msgstr "プロファイルについて詳しく →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "チャンネルから退出する" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "チャンネルを退出" @@ -1411,6 +1544,14 @@ msgstr "プロフィール情報とメタデータを管理する" msgid "Mark as bot - usually 'on' or empty" msgstr "ボットとしてマーク — 通常は「on」または空欄" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "自分を離席中に設定する" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "自分を戻ったと設定する" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "最大ユーザー数" @@ -1573,6 +1714,10 @@ msgstr "ネットワーク名" msgid "New DM" msgstr "新しいDM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "新しいニックネーム" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "次の画像" @@ -1617,6 +1762,10 @@ msgstr "BAN例外が見つかりません" msgid "No bans found" msgstr "BANが見つかりません" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "このネットワークにはまだボットが登録されていません。" + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "中央サーバーなし:" @@ -1672,7 +1821,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" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC — IRCを未来へ" msgid "Off" msgstr "オフ" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "オフライン" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "オフライン" @@ -1754,6 +1908,10 @@ msgstr "オフライン" msgid "on" msgstr "オン" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "次のいずれか:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "おっと!ネットワーク分割が発生しました!⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "ユーザーへのプライベートメッセージを開く" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "チャンネル設定を開く" @@ -1828,10 +1990,23 @@ msgstr "Operパスワード" msgid "Oper Username" msgstr "Operユーザー名" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Operator アクション" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "アプリ改善のための任意のクラッシュレポート" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "送信" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "出力は切り詰められました" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "オーナー" @@ -1994,6 +2169,10 @@ msgstr "退出メッセージ" msgid "Quit the server" msgstr "サーバーを退出しました" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "が実行" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "リアクション" @@ -2024,6 +2203,10 @@ msgstr "理由" msgid "Reason (optional)" msgstr "理由(任意)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "推論" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "サーバーに再接続" @@ -2037,6 +2220,11 @@ msgstr "更新" msgid "Register for an account" msgstr "アカウントを登録する" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "拒否" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "緩め" @@ -2077,11 +2265,27 @@ msgstr "サーバー上でこのチャンネルの名前を変更します。す msgid "Render markdown formatting in messages" msgstr "メッセージ内のMarkdown書式を表示する" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "このメッセージを生成したワークフローを再度開く({stepCount} ステップ)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "返信" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "必須" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "チャットで応答しました" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "応答メッセージは表示されていません" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "再送信" msgid "Rules" msgstr "ルール" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "実行" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "安全" @@ -2121,6 +2329,10 @@ msgstr "保存済み" msgid "Saving..." msgstr "保存中..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "チャットをこの応答までスクロール" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "最下部にスクロール" msgid "Search" msgstr "検索" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "ボットを検索" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "GIFを検索して始めましょう" @@ -2183,6 +2399,10 @@ msgstr "セキュリティ警告" msgid "Seek" msgstr "シーク" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "コマンドと管理アクションを表示するには、左側のボットを選択してください。" + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "チャンネルを選択" @@ -2195,6 +2415,10 @@ msgstr "メンバーを選択" msgid "Select or add a channel to get started." msgstr "チャンネルを選択または追加して始めましょう。" +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "自己登録" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "GIFを送信" msgid "Send a warning message to {username}" msgstr "{username} に警告メッセージを送る" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "アクション / エモートを送信する" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "招待を送信" @@ -2280,6 +2508,10 @@ msgstr "サーバーパスワード" msgid "Server-to-server communication may use unencrypted connections" msgstr "サーバー間通信に暗号化されていない接続が使用される可能性があります" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "サーバー全体" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "トピックを設定…" @@ -2376,6 +2608,10 @@ msgstr "サーバーの信頼済みファイルホストからのメディアを msgid "Signed On" msgstr "サインイン日時" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "スラッシュコマンド" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "ソフトウェア:" @@ -2392,10 +2628,18 @@ msgstr "ユーザー数順で並び替え" msgid "SoundCloud player" msgstr "SoundCloudプレーヤー" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "ソース" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "プライベートメッセージを開始" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "ワークフローを開始しています…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "ステータスメッセージ" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "停止" @@ -2419,10 +2664,18 @@ msgstr "厳格" msgid "Strict - More aggressive protection" msgstr "厳格 — より積極的な保護" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "停止" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "システム" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "テキスト" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "テキストチャンネル" @@ -2447,6 +2700,14 @@ msgstr "このチャンネルに表示されるトピックです。すべての msgid "The user will receive an invitation to join {channelName}." msgstr "ユーザーは {channelName} への参加招待を受け取ります。" +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "このメッセージを生成したワークフローはもう状態にありません" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "このコマンドはパラメータを受け取りません。" + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "この項目は必須です" @@ -2510,6 +2771,10 @@ msgstr "通知を切り替え" msgid "Toggle search" msgstr "検索を切り替え" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "ツール" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "トピック設定日時(以降、分前)" @@ -2527,6 +2792,10 @@ msgstr "トピック:" msgid "Total: {0}" msgstr "合計:{0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "トランスポート" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "信頼できるソース" @@ -2561,6 +2830,10 @@ msgstr "プライベートチャットのピン留めを解除" msgid "Unpin this private message conversation" msgstr "このプライベートメッセージの会話のピン留めを解除する" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "停止を解除" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "アップロード" @@ -2687,6 +2960,11 @@ msgstr "とても緩め" msgid "Very Strict" msgstr "とても厳格" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "@{0} 経由" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "動画" @@ -2730,6 +3008,10 @@ msgstr "Voiceユーザー" msgid "Volume" msgstr "音量" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "最初のステップを待っています…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "チャンネルからキックされました" msgid "We don't store your IRC communications on our servers" msgstr "IRC通信はサーバーに保存されません" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "{__DEFAULT_IRC_SERVER_NAME__} へようこそ!" @@ -2772,6 +3058,10 @@ msgstr "今何してるの?" msgid "What this means:" msgstr "これが意味すること:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "あなたの行動" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "今どんな気分ですか?" @@ -2780,6 +3070,10 @@ msgstr "今どんな気分ですか?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "現在のチャンネルのコンテキストでユーザーにささやく" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "広め — より広範な保護範囲" @@ -2788,6 +3082,19 @@ msgstr "広め — より広範な保護範囲" msgid "Will default to 'no reason' if left empty" msgstr "空欄の場合は「理由なし」がデフォルトになります" +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "ワークフロー履歴" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "ワークフロー履歴({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "処理中…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/ko/messages.mjs b/src/locales/ko/messages.mjs index 067e3cdb..01db4aa6 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>위험: 민감한 정보(메시지, 비공개 대화, 인증 정보)가 네트워크 관리자나 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\"],\"에게 메시지\"],\"chQsxg\":[\"형식화된 출력 복사\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\"에 오신 것을 환영합니다!\"],\"cnGeoo\":[\"삭제\"],\"coPLXT\":[\"IRC 통신 내용은 서버에 저장되지 않습니다\"],\"crYH/6\":[\"SoundCloud 플레이어\"],\"d3sis4\":[\"서버 추가\"],\"d9aN5k\":[\"채널에서 \",[\"username\"],\" 제거\"],\"dEgA5A\":[\"취소\"],\"dGi1We\":[\"이 비공개 메시지 대화 고정 해제\"],\"dJVuyC\":[[\"channelName\"],\"에서 나갔습니다 (\",[\"reason\"],\")\"],\"dMtLDE\":[\"받는 사람\"],\"dXqxlh\":[\"<0>⚠️ 보안 위험! 이 연결은 도청 또는 중간자 공격에 취약할 수 있습니다.\"],\"da9Q/R\":[\"채널 모드 변경됨\"],\"dhJN3N\":[\"댓글 보기\"],\"dj2xTE\":[\"알림 닫기\"],\"dpCzmC\":[\"플러드 방지 설정\"],\"e9dQpT\":[\"이 링크를 새 탭에서 여시겠습니까?\"],\"ePK91l\":[\"편집\"],\"eYBDuB\":[\"이미지를 업로드하거나 동적 크기 조정을 위해 \",[\"size\"],\" 대체가 있는 URL을 제공하세요\"],\"edBbee\":[\"호스트마스크로 \",[\"username\"],\" 차단 (동일 IP/호스트에서 재입장 방지)\"],\"ekfzWq\":[\"사용자 설정\"],\"elPDWs\":[\"IRC 클라이언트 환경을 맞춤 설정하세요\"],\"eu2osY\":[\"<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>⚠️ 주의하세요! 신뢰할 수 있는 출처의 링크만 여세요. 악성 링크는 보안이나 개인정보를 침해할 수 있습니다.\"],\"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\"],\"에 대한 연결에 다음과 같은 보안 문제가 있습니다:\"],\"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/invitation 기능이 광고되지 않음). 일반 채팅은 계속할 수 있으며, 이 패널은 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\":[\"자리 비움 설정 시 기본 메시지\"],\"+fRR7i\":[\"정지\"],\"+mVPqU\":[\"메시지에서 마크다운 서식 렌더링\"],\"+vqCJH\":[\"인증을 위한 계정 사용자 이름\"],\"+yPBXI\":[\"파일 선택\"],\"+zy2Nq\":[\"유형\"],\"/09cao\":[\"낮은 링크 보안 (레벨 \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"돌아옴으로 표시\"],\"/3BQ4J\":[\"채널 외부 사용자는 메시지를 보낼 수 없습니다\"],\"/4C8U0\":[\"모두 복사\"],\"/6BzZF\":[\"멤버 목록 전환\"],\"/AkXyp\":[\"확인하시겠습니까?\"],\"/TNOPk\":[\"자리 비움 중\"],\"/XQgft\":[\"채널 탐색\"],\"/cF7Rs\":[\"볼륨\"],\"/dqduX\":[\"다음 페이지\"],\"/fc3q4\":[\"모든 콘텐츠\"],\"/kISDh\":[\"알림 소리 활성화\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"오디오\"],\"/rfkZe\":[\"멘션 및 메시지에 소리 재생\"],\"/xQ19T\":[\"이 네트워크의 봇\"],\"0/0ZGA\":[\"채널 이름 마스크\"],\"0D6j7U\":[\"사용자 정의 규칙에 대해 더 알아보기 →\"],\"0XsHcR\":[\"사용자 추방\"],\"0ZpE//\":[\"사용자 수순 정렬\"],\"0bEPwz\":[\"자리 비움 설정\"],\"0dGkPt\":[\"채널 목록 펼치기\"],\"0gS7M5\":[\"표시 이름\"],\"0kS+M8\":[\"예시네트워크\"],\"0rgoY7\":[\"직접 선택한 서버에만 연결합니다\"],\"0wdd7X\":[\"참여\"],\"0wkVYx\":[\"비공개 메시지\"],\"111uHX\":[\"링크 미리보기\"],\"196EG4\":[\"비공개 채팅 삭제\"],\"1C/fOn\":[\"봇이 아직 슬래시 명령을 등록하지 않았습니다.\"],\"1DSr1i\":[\"계정 등록\"],\"1O/24y\":[\"채널 목록 전환\"],\"1QfxQT\":[\"닫기\"],\"1VPJJ2\":[\"외부 링크 경고\"],\"1ZC/dv\":[\"읽지 않은 멘션이나 메시지가 없습니다\"],\"1pO1zi\":[\"서버 이름은 필수입니다\"],\"1t/NnN\":[\"거부\"],\"1uwfzQ\":[\"채널 주제 보기\"],\"268g7c\":[\"표시 이름 입력\"],\"2F9+AZ\":[\"아직 캡처된 원시 IRC 트래픽이 없습니다. 연결하거나 메시지를 보내 보세요.\"],\"2FOFq1\":[\"네트워크 서버 운영자가 메시지를 읽을 수 있습니다\"],\"2FYpfJ\":[\"더 보기\"],\"2HF1Y2\":[[\"inviter\"],\"이(가) \",[\"target\"],\"을(를) \",[\"channel\"],\"에 초대했습니다\"],\"2I70QL\":[\"사용자 프로필 정보 보기\"],\"2QYdmE\":[\"사용자:\"],\"2QpEjG\":[\"퇴장했습니다\"],\"2YE223\":[\"#\",[\"0\"],\"에 메시지 (Enter로 줄 바꿈, Shift+Enter로 전송)\"],\"2bimFY\":[\"서버 비밀번호 사용\"],\"2iTmdZ\":[\"로컬 저장소:\"],\"2odkwe\":[\"엄격 - 더 강력한 보호\"],\"2uDhbA\":[\"초대할 사용자 이름 입력\"],\"2xXP/g\":[\"채널 참여\"],\"2ygf/L\":[\"← 뒤로\"],\"2zEgxj\":[\"GIF 검색...\"],\"3JjdaA\":[\"실행\"],\"3NJ4MW\":[\"이 메시지를 생성한 워크플로 다시 열기 (\",[\"stepCount\"],\"단계)\"],\"3RdPhl\":[\"채널 이름 변경\"],\"3THokf\":[\"Voice 사용자\"],\"3TSz9S\":[\"최소화\"],\"3et0TM\":[\"이 응답으로 채팅 스크롤\"],\"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\":[\"짧은 메시지의 경우 자동으로 한 줄 모드로 전환\"],\"4uKgKr\":[\"송신\"],\"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\":[\"개인정보 보호에 관한 문의사항이 있으신가요? 문의처:\"],\"9q17ZR\":[[\"0\"],\"은(는) 필수입니다.\"],\"9qIYMn\":[\"새 닉네임\"],\"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\":[\"저장되지 않은 변경 사항이 있습니다. 저장하지 않고 닫으시겠습니까?\"],\"BIDT9R\":[\"봇\"],\"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 표시\"],\"CaQ1Gb\":[\"설정으로 정의된 봇입니다. 상태를 변경하려면 obbyircd.conf를 편집하고 /REHASH를 실행하세요.\"],\"CbvaYj\":[\"닉네임으로 차단\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"채널 선택\"],\"CsekCi\":[\"기본\"],\"D+NlUC\":[\"시스템\"],\"D28t6+\":[\"참여했다가 퇴장했습니다\"],\"DB8zMK\":[\"적용\"],\"DBcWHr\":[\"사용자 정의 알림 소리 파일\"],\"DSHF2K\":[\"이 메시지를 생성한 워크플로가 더 이상 상태에 없습니다\"],\"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\":[\"서버 검색...\"],\"Eu7YKa\":[\"자가 등록됨\"],\"F0OKMc\":[\"서버 편집\"],\"F6Int2\":[\"강조 표시 활성화\"],\"FDoLyE\":[\"최대 사용자 수\"],\"FUU/hZ\":[\"채팅에서 로드할 외부 미디어의 범위를 제어하세요.\"],\"Fdp03t\":[\"켜기\"],\"FfPWR0\":[\"모달\"],\"FjkaiT\":[\"축소\"],\"FlqOE9\":[\"이것이 의미하는 바:\"],\"FolHNl\":[\"계정 및 인증 관리\"],\"Fp2Dif\":[\"서버에서 나갔습니다\"],\"G5KmCc\":[\"GZ-Line (글로벌 Z-Line)\"],\"GDs0lz\":[\"<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\"],\"님이 입력 중...\"],\"IcHxhR\":[\"오프라인\"],\"IgrLD/\":[\"일시 정지\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"답장\"],\"IoHMnl\":[\"최대값은 \",[\"0\"],\"입니다\"],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"연결 중...\"],\"J5T9NW\":[\"사용자 정보\"],\"J8Y5+z\":[\"이런! 네트워크 분리! ⚠️\"],\"JBHkBA\":[\"채널을 나갔습니다\"],\"JCwL0Q\":[\"사유 입력 (선택 사항)\"],\"JFciKP\":[\"전환\"],\"JMXMCX\":[\"자리 비움 메시지\"],\"JXGkhG\":[\"채널 이름 변경 (운영자 전용)\"],\"JYiL1b\":[\"다음 중 하나:\"],\"JcD7qf\":[\"추가 작업\"],\"JdkA+c\":[\"비밀 채널 (+s)\"],\"Jmu12l\":[\"서버 채널\"],\"JvQ++s\":[\"마크다운 활성화\"],\"K2jwh/\":[\"WHOIS 데이터를 사용할 수 없습니다\"],\"K4vEhk\":[\"(정지됨)\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"메시지 삭제\"],\"KKBlUU\":[\"임베드\"],\"KM0pLb\":[\"채널에 오신 것을 환영합니다!\"],\"KR6W2h\":[\"사용자 무시 해제\"],\"KV+Bi1\":[\"초대 전용 (+i)\"],\"KdCtwE\":[\"카운터를 초기화하기 전에 플러드 활동을 모니터링할 초 단위 시간\"],\"Kkezga\":[\"서버 비밀번호\"],\"KsiQ/8\":[\"채널 참여를 위해 초대가 필요합니다\"],\"KtADxr\":[\"실행함\"],\"L+gB/D\":[\"채널 정보\"],\"LC1a7n\":[\"IRC 서버에서 서버 간 링크의 보안 수준이 낮다고 보고했습니다. 즉, 메시지가 네트워크의 IRC 서버 간에 전달될 때 적절히 암호화되지 않거나 SSL/TLS 인증서가 올바르게 검증되지 않을 수 있습니다.\"],\"LN3RO2\":[[\"0\"],\"단계가 승인 대기 중\"],\"LNfLR5\":[\"추방 표시\"],\"LQb0W/\":[\"모든 이벤트 표시\"],\"LU7/yA\":[\"UI에 표시할 대체 이름입니다. 공백, 이모지, 특수 문자를 포함할 수 있습니다. IRC 명령에는 실제 채널 이름(\",[\"channelName\"],\")이 사용됩니다.\"],\"LUb9O7\":[\"올바른 서버 포트가 필요합니다\"],\"LV4fT6\":[\"설명 (선택사항, 예: \\\"3분기 베타 테스터\\\")\"],\"LYzbQ2\":[\"도구\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"개인정보 처리방침\"],\"LcuSDR\":[\"프로필 정보 및 메타데이터 관리\"],\"LqLS9B\":[\"닉네임 변경 표시\"],\"LsDQt2\":[\"채널 설정\"],\"LtI9AS\":[\"소유자\"],\"LuNhhL\":[\"이 메시지에 반응했습니다\"],\"M/AZNG\":[\"아바타 이미지의 URL\"],\"M/WIer\":[\"메시지 보내기\"],\"M45wtf\":[\"이 명령어는 매개변수를 받지 않습니다.\"],\"M8er/5\":[\"이름:\"],\"MHk+7g\":[\"이전 이미지\"],\"MRorGe\":[\"사용자에게 PM\"],\"MVbSGP\":[\"시간 창 (초)\"],\"MkpcsT\":[\"메시지와 설정은 기기에 로컬로 저장됩니다\"],\"N/hDSy\":[\"봇으로 표시 - 보통 'on' 또는 비워두기\"],\"N40H+G\":[\"전체\"],\"N7TQbE\":[[\"channelName\"],\"에 사용자 초대\"],\"NCca/o\":[\"기본 닉네임 입력...\"],\"NQN2HS\":[\"정지 해제\"],\"Nqs6B9\":[\"모든 외부 미디어를 표시합니다. 모든 URL이 알 수 없는 서버에 요청을 보낼 수 있습니다.\"],\"Nt+9O7\":[\"원시 TCP 대신 WebSocket 사용\"],\"NxIHzc\":[\"사용자 연결 끊기\"],\"O+HhhG\":[\"현재 채널에서 사용자에게 귓속말 보내기\"],\"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\"]],\"RlCInP\":[\"슬래시 명령\"],\"RySp6q\":[\"댓글 숨기기\"],\"RzfkXn\":[\"이 서버에서 닉네임 변경\"],\"SPKQTd\":[\"닉네임은 필수입니다\"],\"SPVjfj\":[\"비워두면 기본값인 '사유 없음'이 사용됩니다\"],\"SQKPvQ\":[\"사용자 초대\"],\"SkZcl+\":[\"미리 정의된 플러드 방지 프로필을 선택하세요. 각 프로필은 다양한 사용 사례에 맞게 균형 잡힌 보호 설정을 제공합니다.\"],\"Slr+3C\":[\"최소 사용자 수\"],\"Spnlre\":[[\"target\"],\"을(를) \",[\"channel\"],\"에 초대했습니다\"],\"T/ckN5\":[\"뷰어에서 열기\"],\"T91vKp\":[\"재생\"],\"TImSWn\":[\"(ObsidianIRC가 처리)\"],\"TRDppN\":[\"Webhook\"],\"TV2Wdu\":[\"데이터 처리 방식 및 개인정보 보호 방법을 알아보세요.\"],\"TgFpwD\":[\"적용 중...\"],\"TkzSFB\":[\"변경 사항 없음\"],\"TtserG\":[\"실명 입력\"],\"Ttz9J1\":[\"비밀번호 입력...\"],\"Tz0i8g\":[\"설정\"],\"U3pytU\":[\"관리자\"],\"U7yg75\":[\"자리 비움으로 표시\"],\"UDb2YD\":[\"반응\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"아직 초대 링크를 생성하지 않았습니다. 위 양식을 사용하여 첫 링크를 만들어보세요.\"],\"UGT5vp\":[\"설정 저장\"],\"UV5hLB\":[\"차단된 사용자가 없습니다\"],\"Uaj3Nd\":[\"상태 메시지\"],\"Ue3uny\":[\"기본값 (프로필 없음)\"],\"UkARhe\":[\"기본 - 표준 보호\"],\"Umn7Cj\":[\"아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!\"],\"UqtiKk\":[[\"secondsLeft\"],\"초 후 자동으로 닫힘\"],\"UrEy4W\":[\"사용자에게 개인 메시지 열기\"],\"UtUIRh\":[\"이전 메시지 \",[\"0\"],\"개\"],\"UwzP+U\":[\"보안 연결\"],\"V0/A4O\":[\"채널 소유자\"],\"V2dwib\":[[\"0\"],\"은(는) 숫자여야 합니다.\"],\"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\":[\"비공개 채팅 고정 해제\"],\"XklovM\":[\"작업 중…\"],\"Xm/s+u\":[\"화면 표시\"],\"Xp2n93\":[\"서버의 신뢰할 수 있는 파일 호스트의 미디어를 표시합니다. 외부 서비스에 요청이 전송되지 않습니다.\"],\"XvjC4F\":[\"저장 중...\"],\"Y+tK3n\":[\"보낼 첫 메시지\"],\"Y/qryO\":[\"검색 결과와 일치하는 사용자가 없습니다\"],\"YAqRpI\":[[\"account\"],\" 계정 등록 성공: \",[\"message\"]],\"YBXJ7j\":[\"수신\"],\"YEfzvP\":[\"주제 보호 (+t)\"],\"YQOn6a\":[\"멤버 목록 접기\"],\"YRCoE9\":[\"채널 Op\"],\"YURQaF\":[\"프로필 보기\"],\"YdBSvr\":[\"미디어 표시 및 외부 콘텐츠 제어\"],\"Yj6U3V\":[\"중앙 서버 없음:\"],\"YjvpGx\":[\"대명사\"],\"YqH4l4\":[\"키 없음\"],\"YyUPpV\":[\"계정:\"],\"Z7ZXbT\":[\"승인\"],\"ZJSWfw\":[\"서버 연결 종료 시 표시할 메시지\"],\"ZR1dJ4\":[\"초대\"],\"ZdWg0V\":[\"브라우저에서 열기\"],\"ZhRBbl\":[\"메시지 검색…\"],\"Zmcu3y\":[\"고급 필터\"],\"ZqLD8l\":[\"서버 전체\"],\"a2/8e5\":[\"주제 설정 이후 (분 전)\"],\"aHKcKc\":[\"이전 페이지\"],\"aJTbXX\":[\"Oper 비밀번호\"],\"aP9gNu\":[\"출력이 잘림\"],\"aQryQv\":[\"이미 존재하는 패턴입니다\"],\"aW9pLN\":[\"채널에 허용되는 최대 사용자 수입니다. 제한 없이 두려면 비워두세요.\"],\"ah4fmZ\":[\"YouTube, Vimeo, SoundCloud 등 알려진 서비스의 미리보기도 표시합니다.\"],\"aifXak\":[\"이 채널에 미디어가 없습니다\"],\"ap2zBz\":[\"완화\"],\"az8lvo\":[\"끄기\"],\"azXSNo\":[\"멤버 목록 펼치기\"],\"azdliB\":[\"계정에 로그인\"],\"b26wlF\":[\"그녀/그녀의\"],\"bD/+Ei\":[\"엄격\"],\"bFDO8z\":[\"gateway 온라인\"],\"bQ6BJn\":[\"상세한 플러드 방지 규칙을 설정하세요. 각 규칙은 모니터링할 활동 유형과 임계값 초과 시 취할 조치를 지정합니다.\"],\"bVBC/W\":[\"Gateway 연결됨\"],\"beV7+y\":[\"사용자가 \",[\"channelName\"],\" 참여 초대를 받게 됩니다.\"],\"bk84cH\":[\"자리 비움 메시지\"],\"bkHdLj\":[\"IRC 서버 추가\"],\"bmQLn5\":[\"규칙 추가\"],\"bv4cFj\":[\"전송\"],\"bwRvnp\":[\"작업\"],\"c8+EVZ\":[\"인증된 계정\"],\"cGYUlD\":[\"미디어 미리보기가 로드되지 않습니다.\"],\"cLF98o\":[\"댓글 보기 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"사용 가능한 사용자가 없습니다\"],\"cSgpoS\":[\"비공개 채팅 고정\"],\"cde3ce\":[\"<0>\",[\"0\"],\"에게 메시지\"],\"chQsxg\":[\"형식화된 출력 복사\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\"에 오신 것을 환영합니다!\"],\"cnGeoo\":[\"삭제\"],\"coPLXT\":[\"IRC 통신 내용은 서버에 저장되지 않습니다\"],\"crYH/6\":[\"SoundCloud 플레이어\"],\"d3sis4\":[\"서버 추가\"],\"d9aN5k\":[\"채널에서 \",[\"username\"],\" 제거\"],\"dEgA5A\":[\"취소\"],\"dGi1We\":[\"이 비공개 메시지 대화 고정 해제\"],\"dJVuyC\":[[\"channelName\"],\"에서 나갔습니다 (\",[\"reason\"],\")\"],\"dMtLDE\":[\"받는 사람\"],\"dRqrdL\":[[\"0\"],\"은(는) 정수여야 합니다.\"],\"dXqxlh\":[\"<0>⚠️ 보안 위험! 이 연결은 도청 또는 중간자 공격에 취약할 수 있습니다.\"],\"da9Q/R\":[\"채널 모드 변경됨\"],\"dhJN3N\":[\"댓글 보기\"],\"dj2xTE\":[\"알림 닫기\"],\"dnUmOX\":[\"아직 이 네트워크에 등록된 봇이 없습니다.\"],\"dpCzmC\":[\"플러드 방지 설정\"],\"e7KzRG\":[[\"0\"],\"단계\"],\"e9dQpT\":[\"이 링크를 새 탭에서 여시겠습니까?\"],\"ePK91l\":[\"편집\"],\"eYBDuB\":[\"이미지를 업로드하거나 동적 크기 조정을 위해 \",[\"size\"],\" 대체가 있는 URL을 제공하세요\"],\"edBbee\":[\"호스트마스크로 \",[\"username\"],\" 차단 (동일 IP/호스트에서 재입장 방지)\"],\"ekfzWq\":[\"사용자 설정\"],\"elPDWs\":[\"IRC 클라이언트 환경을 맞춤 설정하세요\"],\"eu2osY\":[\"<0>💡 권장사항: 이 서버를 신뢰하고 위험을 충분히 이해한 경우에만 계속하세요. 이 연결을 통해 민감한 정보나 비밀번호를 공유하지 마세요.\"],\"euEhbr\":[[\"channel\"],\"에 참여하려면 클릭\"],\"ez3vLd\":[\"여러 줄 입력 활성화\"],\"f0J5Ki\":[\"서버 간 통신에 암호화되지 않은 연결이 사용될 수 있습니다\"],\"f9BHJk\":[\"사용자 경고\"],\"fDOLLd\":[\"채널을 찾을 수 없습니다.\"],\"fYdEvu\":[\"워크플로 기록 (\",[\"0\"],\")\"],\"ffzDkB\":[\"익명 분석:\"],\"fq1GF9\":[\"사용자가 서버에서 연결을 끊을 때 표시\"],\"gEF57C\":[\"이 서버는 하나의 연결 유형만 지원합니다\"],\"gJuLUI\":[\"무시 목록\"],\"gNzMrk\":[\"현재 아바타\"],\"gjPWyO\":[\"닉네임 입력...\"],\"gz6UQ3\":[\"최대화\"],\"h6razj\":[\"채널 이름 마스크 제외\"],\"hG6jnw\":[\"설정된 주제 없음\"],\"hG89Ed\":[\"이미지\"],\"hYgDIe\":[\"생성\"],\"hZ6znB\":[\"포트\"],\"ha+Bz5\":[\"예: 100:1440\"],\"hctjqj\":[\"왼쪽에서 봇을 선택하면 해당 명령과 관리 작업을 볼 수 있습니다.\"],\"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\":[\"아바타 업로드\"],\"jUXib7\":[\"응답 메시지가 더 이상 표시되지 않습니다\"],\"jW5Uwh\":[\"로드할 외부 미디어의 범위를 제어합니다. 끄기 / 안전 / 신뢰할 수 있는 출처 / 모든 콘텐츠.\"],\"jXzms5\":[\"첨부 옵션\"],\"jZlrte\":[\"색상\"],\"jfC/xh\":[\"연락처\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"이전 메시지 불러오기\"],\"k3ID0F\":[\"멤버 필터링…\"],\"k65gsE\":[\"자세히 보기\"],\"k7Zgob\":[\"연결 취소\"],\"kAVx5h\":[\"초대가 없습니다\"],\"kCLEPU\":[\"연결된 서버\"],\"kF5LKb\":[\"무시된 패턴:\"],\"kG2fiE\":[\"설정 정의됨\"],\"kGeOx/\":[[\"0\"],\" 참가\"],\"kITKr8\":[\"채널 모드 불러오는 중...\"],\"kPpPsw\":[\"당신은 IRC Operator입니다\"],\"kWJmRL\":[\"나\"],\"kfcRb0\":[\"아바타\"],\"kjMqSj\":[\"JSON 복사\"],\"krViRy\":[\"JSON으로 복사하려면 클릭\"],\"ks71ra\":[\"예외\"],\"kw4lRv\":[\"채널 Halfop\"],\"kxgIRq\":[\"시작하려면 채널을 선택하거나 추가하세요.\"],\"ky2mw7\":[\"@\",[\"0\"],\" 통해\"],\"ky6dWe\":[\"아바타 미리보기\"],\"l+GxCv\":[\"채널 불러오는 중...\"],\"l+IUVW\":[[\"account\"],\" 계정 인증 성공: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[[\"reconnectCount\"],\"번 재연결됨\"]}]],\"l1l8sj\":[[\"0\"],\"일 전\"],\"l5NhnV\":[\"#채널 (선택사항)\"],\"l5jmzx\":[[\"0\"],\"님과 \",[\"1\"],\"님이 입력 중...\"],\"lCF0wC\":[\"새로 고침\"],\"lH+ed1\":[\"첫 단계 대기 중…\"],\"lHy8N5\":[\"채널 더 불러오는 중...\"],\"lasgrr\":[\"사용됨\"],\"lbpf14\":[[\"value\"],\" 참여\"],\"lf3MT4\":[\"나갈 채널 (기본값: 현재 채널)\"],\"lfFsZ4\":[\"채널\"],\"lkNdiH\":[\"계정 이름\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"이미지 업로드\"],\"loQxaJ\":[\"돌아왔습니다\"],\"lvfaxv\":[\"홈\"],\"m16xKo\":[\"추가\"],\"m8flAk\":[\"미리보기 (아직 업로드되지 않음)\"],\"mDkV0w\":[\"워크플로 시작 중…\"],\"mEPxTp\":[\"<0>⚠️ 주의하세요! 신뢰할 수 있는 출처의 링크만 여세요. 악성 링크는 보안이나 개인정보를 침해할 수 있습니다.\"],\"mHGdhG\":[\"서버 정보\"],\"mHS8lb\":[\"#\",[\"0\"],\"에 메시지 보내기\"],\"mHfd/S\":[\"현재 하고 있는 일\"],\"mMYBD9\":[\"광역 - 더 넓은 보호 범위\"],\"mTGsPd\":[\"채널 주제\"],\"mU8j6O\":[\"외부 메시지 차단 (+n)\"],\"mZp8FL\":[\"자동으로 한 줄 모드로 전환\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"댓글 (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"인증된 사용자\"],\"mwtcGl\":[\"댓글 닫기\"],\"mzI/c+\":[\"다운로드\"],\"n3fGRk\":[[\"0\"],\"이(가) 설정\"],\"nE9jsU\":[\"완화 - 덜 공격적인 보호\"],\"nNflMD\":[\"채널 나가기\"],\"nPXkBi\":[\"WHOIS 데이터 불러오는 중...\"],\"nQnxxF\":[\"#\",[\"0\"],\"에 메시지 (Shift+Enter로 줄 바꿈)\"],\"nWMRxa\":[\"고정 해제\"],\"nX4XLG\":[\"운영자 작업\"],\"nkC032\":[\"플러드 프로필 없음\"],\"o69z4d\":[[\"username\"],\"에게 경고 메시지 보내기\"],\"o9ylQi\":[\"GIF를 검색하여 시작하세요\"],\"oFGkER\":[\"서버 알림\"],\"oOi11l\":[\"맨 아래로 스크롤\"],\"oPYIL5\":[\"네트워크\"],\"oQEzQR\":[\"새 DM\"],\"oXOSPE\":[\"온라인\"],\"oaTtrx\":[\"봇 검색\"],\"oal760\":[\"서버 링크에 대한 중간자 공격이 가능합니다\"],\"oeqmmJ\":[\"신뢰할 수 있는 출처\"],\"optX0N\":[[\"0\"],\"시간 전\"],\"ovBPCi\":[\"기본값\"],\"p0Z69r\":[\"패턴은 비워둘 수 없습니다\"],\"p1KgtK\":[\"오디오를 불러오지 못했습니다\"],\"p59pEv\":[\"추가 세부정보\"],\"p7sRI6\":[\"입력 중임을 다른 사람에게 알림\"],\"pBm1od\":[\"비밀 채널\"],\"pNmiXx\":[\"모든 서버에 사용할 기본 닉네임\"],\"pQBYsE\":[\"채팅으로 응답함\"],\"pUUo9G\":[\"호스트명:\"],\"pVGPmz\":[\"계정 비밀번호\"],\"peNE68\":[\"영구\"],\"plhHQt\":[\"데이터 없음\"],\"pm6+q5\":[\"보안 경고\"],\"pn5qSs\":[\"추가 정보\"],\"q0cR4S\":[\"이제 **\",[\"newNick\"],\"**(으)로 알려져 있습니다\"],\"qFcunY\":[\"LIST 또는 NAMES 명령에 채널이 표시되지 않음\"],\"qLpTm/\":[[\"emoji\"],\" 반응 제거\"],\"qVkGWK\":[\"고정\"],\"qXgujk\":[\"액션 / 감정표현 보내기\"],\"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\"],\"에 대한 연결에 다음과 같은 보안 문제가 있습니다:\"],\"sGH11W\":[\"서버\"],\"sHI1H+\":[\"이제 **\",[\"newNick\"],\"**(으)로 알려져 있습니다\"],\"sJyV04\":[[\"inviter\"],\"이(가) 당신을 \",[\"channel\"],\"에 초대했습니다\"],\"sW5OjU\":[\"필수\"],\"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\":[\"저장\"],\"thC9Rq\":[\"채널 나가기\"],\"tiBsJk\":[[\"channelName\"],\"에서 나갔습니다\"],\"tt4/UD\":[\"서버를 나갔습니다 (\",[\"reason\"],\")\"],\"tu2JEr\":[\"참여할 채널 (#이름)\"],\"u0TcnO\":[\"닉네임 {nick}이(가) 이미 사용 중입니다. {newNick}(으)로 다시 시도합니다\"],\"u0a8B4\":[\"관리 권한을 위해 IRC Operator로 인증\"],\"u0rWFU\":[\"생성 시각 이후 (분 전)\"],\"u72w3t\":[\"무시할 사용자 및 패턴\"],\"u7jc2L\":[\"서버를 나갔습니다\"],\"uAQUqI\":[\"상태\"],\"uB85T3\":[\"저장 실패: \",[\"msg\"]],\"uMIUx8\":[\"봇 \",[\"0\"],\"을(를) 삭제하시겠습니까? 데이터베이스 행을 소프트 삭제합니다. 닉네임을 나중에 다시 사용하려면 /REHASH 후에만 가능합니다.\"],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC 서버:\"],\"ukyW4o\":[\"내 초대 링크\"],\"usSSr/\":[\"확대/축소 수준\"],\"v7uvcf\":[\"소프트웨어:\"],\"vE8kb+\":[\"줄 바꿈은 Shift+Enter (Enter로 전송)\"],\"vERlcd\":[\"프로필\"],\"vK0RL8\":[\"주제 없음\"],\"vSJd18\":[\"동영상\"],\"vXIe7J\":[\"언어\"],\"vaHYxN\":[\"실명\"],\"vhjbKr\":[\"자리 비움\"],\"w4NYox\":[[\"title\"],\" 클라이언트\"],\"w8xQRx\":[\"잘못된 값\"],\"wCKe3+\":[\"워크플로 기록\"],\"wFjjxZ\":[[\"username\"],\"에 의해 \",[\"channelName\"],\"에서 추방당했습니다 (\",[\"reason\"],\")\"],\"wGjaGl\":[\"차단 예외가 없습니다\"],\"wPrGnM\":[\"채널 관리자\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"추론\"],\"wbm86v\":[\"사용자가 채널에 입장하거나 퇴장할 때 표시\"],\"wdxz7K\":[\"소스\"],\"whqZ9r\":[\"강조할 추가 단어 또는 문구\"],\"wm7RV4\":[\"알림 소리\"],\"wz/Yoq\":[\"서버 간 전달 시 메시지가 도청될 수 있습니다\"],\"x3+y8b\":[\"이 링크를 통해 등록한 사람 수\"],\"xCJdfg\":[\"지우기\"],\"xOTzt5\":[\"방금\"],\"xUHRTR\":[\"연결 시 자동으로 operator로 인증\"],\"xWHwwQ\":[\"차단 목록\"],\"xYilR2\":[\"미디어\"],\"xbi8D6\":[\"이 서버는 초대 링크를 지원하지 않습니다 (<0>obby.world/invitation 기능이 광고되지 않음). 일반 채팅은 계속할 수 있으며, 이 패널은 obbyircd 기반 네트워크용입니다.\"],\"xceQrO\":[\"보안 웹소켓만 지원됩니다\"],\"xdtXa+\":[\"채널-이름\"],\"xeiujy\":[\"텍스트\"],\"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\"]],\"zPBDzU\":[\"워크플로 취소\"],\"zbymaY\":[[\"0\"],\"분 전\"],\"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..1cf12f2c 100644 --- a/src/locales/ko/messages.po +++ b/src/locales/ko/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - IRC를 미래로" msgid "— open in viewer" msgstr "— 뷰어에서 열기" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(ObsidianIRC가 처리)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +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); @@ -41,16 +49,41 @@ msgstr "{0, plural, other {{1}개 더 보기}}" msgid "{0} and {1} are typing..." msgstr "{0}님과 {1}님이 입력 중..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0}은(는) 필수입니다." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0}님이 입력 중..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0}은(는) 숫자여야 합니다." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0}은(는) 정수여야 합니다." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "이전 메시지 {0}개" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0}단계" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0}단계가 승인 대기 중" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "고급 필터" msgid "Album" msgstr "앨범" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "전체" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "모든 콘텐츠" @@ -297,6 +334,12 @@ msgstr "필터 적용 및 새로 고침" msgid "Applying..." msgstr "적용 중..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "승인" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "인증된 계정" msgid "Auto Fallback to Single Line" msgstr "자동으로 한 줄 모드로 전환" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "{secondsLeft}초 후 자동으로 닫힘" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "연결 시 자동으로 operator로 인증" @@ -359,6 +406,10 @@ msgstr "자리 비움" msgid "Away from keyboard" msgstr "자리 비움 (AFK)" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "자리 비움 메시지" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "자리 비움 메시지" msgid "Away:" msgstr "자리비움:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "차단 목록" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "봇" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "봇이 아직 슬래시 명령을 등록하지 않았습니다." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "봇" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "이 네트워크의 봇" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "서버의 모든 채널 검색" @@ -430,6 +499,7 @@ msgstr "서버의 모든 채널 검색" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "연결 취소" msgid "Cancel reply" msgstr "답장 취소" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "워크플로 취소" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "채널 이름 변경 (운영자 전용)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "이 서버에서 닉네임 변경" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "채널 모드 변경됨" @@ -459,6 +537,7 @@ msgstr "닉네임 변경됨" msgid "changed the topic to: {topic}" msgstr "주제를 변경했습니다: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "채널" @@ -519,6 +598,14 @@ msgstr "채널 소유자" msgid "Channel Settings" msgstr "채널 설정" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "참여할 채널 (#이름)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "나갈 채널 (기본값: 현재 채널)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "채널 주제" @@ -531,6 +618,7 @@ msgstr "LIST 또는 NAMES 명령에 채널이 표시되지 않음" msgid "channel-name" msgstr "채널-이름" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "채널" @@ -606,6 +694,9 @@ msgstr "클라이언트 제한 (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "댓글" msgid "Comments ({commentCount})" msgstr "댓글 ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "설정 정의됨" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "설정으로 정의된 봇입니다. 상태를 변경하려면 obbyircd.conf를 편집하고 /REHASH를 실행하세요." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "상세한 플러드 방지 규칙을 설정하세요. 각 규칙은 모니터링할 활동 유형과 임계값 초과 시 취할 조치를 지정합니다." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "기본 닉네임" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "삭제" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "봇 {0}을(를) 삭제하시겠습니까? 데이터베이스 행을 소프트 삭제합니다. 닉네임을 나중에 다시 사용하려면 /REHASH 후에만 가능합니다." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "채널 삭제" @@ -843,6 +948,10 @@ msgstr "채널 탐색" msgid "Discover the world of IRC with ObsidianIRC" msgstr "ObsidianIRC와 함께 IRC의 세계를 탐험하세요" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "닫기" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "알림 닫기" @@ -891,7 +1000,7 @@ msgstr "다운로드" #: src/components/layout/ChatArea.tsx msgid "Drop files to upload" -msgstr "업로드할 파일을 끌어다 놓으세요" +msgstr "업로드할 파일을 여기에 놓으세요" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "채널 필터링..." msgid "Filter members…" msgstr "멤버 필터링…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "보낼 첫 메시지" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "플러드 프로필 (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (글로벌 밴)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway 연결됨" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway 온라인" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "일반" @@ -1186,6 +1307,10 @@ msgstr "{1}개 중 {0}번째 이미지" msgid "Image preview" msgstr "이미지 미리보기" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "수신" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "정보:" @@ -1270,6 +1395,10 @@ msgstr "{0} 참가" msgid "Join {value}" msgstr "{value} 참여" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "채널 참여" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "사용자 정의 규칙에 대해 더 알아보기 →" msgid "Learn more about profiles →" msgstr "프로필에 대해 더 알아보기 →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "채널 나가기" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "채널 나가기" @@ -1411,6 +1544,14 @@ msgstr "프로필 정보 및 메타데이터 관리" msgid "Mark as bot - usually 'on' or empty" msgstr "봇으로 표시 - 보통 'on' 또는 비워두기" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "자리 비움으로 표시" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "돌아옴으로 표시" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "최대 사용자 수" @@ -1573,6 +1714,10 @@ msgstr "네트워크 이름" msgid "New DM" msgstr "새 DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "새 닉네임" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "다음 이미지" @@ -1617,6 +1762,10 @@ msgstr "차단 예외가 없습니다" msgid "No bans found" msgstr "차단된 사용자가 없습니다" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "아직 이 네트워크에 등록된 봇이 없습니다." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "중앙 서버 없음:" @@ -1672,7 +1821,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" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - IRC를 미래로" msgid "Off" msgstr "끄기" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "오프라인" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "오프라인" @@ -1754,6 +1908,10 @@ msgstr "오프라인" msgid "on" msgstr "켜기" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "다음 중 하나:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "이런! 네트워크 분리! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "사용자에게 개인 메시지 열기" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "채널 구성 설정 열기" @@ -1828,10 +1990,23 @@ msgstr "Oper 비밀번호" msgid "Oper Username" msgstr "Oper 사용자 이름" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "운영자 작업" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "앱 개선을 위한 선택적 충돌 보고서" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "송신" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "출력이 잘림" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "소유자" @@ -1994,6 +2169,10 @@ msgstr "QUIT 메시지" msgid "Quit the server" msgstr "서버에서 나갔습니다" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "실행함" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "반응" @@ -2024,6 +2203,10 @@ msgstr "사유" msgid "Reason (optional)" msgstr "사유 (선택 사항)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "추론" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "서버에 재연결" @@ -2037,6 +2220,11 @@ msgstr "새로 고침" msgid "Register for an account" msgstr "계정 등록" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "거부" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "완화" @@ -2077,11 +2265,27 @@ msgstr "서버에서 이 채널의 이름을 변경합니다. 모든 사용자 msgid "Render markdown formatting in messages" msgstr "메시지에서 마크다운 서식 렌더링" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "이 메시지를 생성한 워크플로 다시 열기 ({stepCount}단계)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "답장" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "필수" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "채팅으로 응답함" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "응답 메시지가 더 이상 표시되지 않습니다" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "다시 보내기" msgid "Rules" msgstr "규칙" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "실행" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "안전" @@ -2121,6 +2329,10 @@ msgstr "저장됨" msgid "Saving..." msgstr "저장 중..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "이 응답으로 채팅 스크롤" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "맨 아래로 스크롤" msgid "Search" msgstr "검색" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "봇 검색" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "GIF를 검색하여 시작하세요" @@ -2183,6 +2399,10 @@ msgstr "보안 경고" msgid "Seek" msgstr "탐색" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "왼쪽에서 봇을 선택하면 해당 명령과 관리 작업을 볼 수 있습니다." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "채널 선택" @@ -2195,6 +2415,10 @@ msgstr "멤버 선택" msgid "Select or add a channel to get started." msgstr "시작하려면 채널을 선택하거나 추가하세요." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "자가 등록됨" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "GIF 보내기" msgid "Send a warning message to {username}" msgstr "{username}에게 경고 메시지 보내기" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "액션 / 감정표현 보내기" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "초대 보내기" @@ -2280,6 +2508,10 @@ msgstr "서버 비밀번호" msgid "Server-to-server communication may use unencrypted connections" msgstr "서버 간 통신에 암호화되지 않은 연결이 사용될 수 있습니다" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "서버 전체" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "주제 설정…" @@ -2376,6 +2608,10 @@ msgstr "서버의 신뢰할 수 있는 파일 호스트의 미디어를 표시 msgid "Signed On" msgstr "접속 시각" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "슬래시 명령" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "소프트웨어:" @@ -2392,10 +2628,18 @@ msgstr "사용자 수순 정렬" msgid "SoundCloud player" msgstr "SoundCloud 플레이어" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "소스" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "비공개 메시지 시작" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "워크플로 시작 중…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "상태 메시지" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "정지" @@ -2419,10 +2664,18 @@ msgstr "엄격" msgid "Strict - More aggressive protection" msgstr "엄격 - 더 강력한 보호" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "정지" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "시스템" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "텍스트" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "텍스트 채널" @@ -2447,6 +2700,14 @@ msgstr "이 채널에 표시될 주제입니다. 모든 사용자가 주제를 msgid "The user will receive an invitation to join {channelName}." msgstr "사용자가 {channelName} 참여 초대를 받게 됩니다." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "이 메시지를 생성한 워크플로가 더 이상 상태에 없습니다" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "이 명령어는 매개변수를 받지 않습니다." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "이 필드는 필수입니다" @@ -2510,6 +2771,10 @@ msgstr "알림 전환" msgid "Toggle search" msgstr "검색 전환" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "도구" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "주제 설정 이후 (분 전)" @@ -2527,6 +2792,10 @@ msgstr "주제:" msgid "Total: {0}" msgstr "전체: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "전송" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "신뢰할 수 있는 출처" @@ -2561,6 +2830,10 @@ msgstr "비공개 채팅 고정 해제" msgid "Unpin this private message conversation" msgstr "이 비공개 메시지 대화 고정 해제" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "정지 해제" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "업로드" @@ -2687,6 +2960,11 @@ msgstr "매우 완화" msgid "Very Strict" msgstr "매우 엄격" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "@{0} 통해" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "동영상" @@ -2730,6 +3008,10 @@ msgstr "Voice 사용자" msgid "Volume" msgstr "볼륨" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "첫 단계 대기 중…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "채널에서 추방되었습니다" msgid "We don't store your IRC communications on our servers" msgstr "IRC 통신 내용은 서버에 저장되지 않습니다" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "{__DEFAULT_IRC_SERVER_NAME__}에 오신 것을 환영합니다!" @@ -2772,6 +3058,10 @@ msgstr "지금 뭐 하세요?" msgid "What this means:" msgstr "이것이 의미하는 바:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "현재 하고 있는 일" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "무슨 생각을 하고 계신가요?" @@ -2780,6 +3070,10 @@ msgstr "무슨 생각을 하고 계신가요?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "현재 채널에서 사용자에게 귓속말 보내기" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "광역 - 더 넓은 보호 범위" @@ -2788,6 +3082,19 @@ msgstr "광역 - 더 넓은 보호 범위" msgid "Will default to 'no reason' if left empty" msgstr "비워두면 기본값인 '사유 없음'이 사용됩니다" +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "워크플로 기록" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "워크플로 기록 ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "작업 중…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/nl/messages.mjs b/src/locales/nl/messages.mjs index 495f055d..83c59608 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: 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\"],\"\"],\"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! 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: 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! 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\"],\" 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/invitation-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\"],\"+fRR7i\":[\"Opschorten\"],\"+mVPqU\":[\"Markdown-opmaak in berichten weergeven\"],\"+vqCJH\":[\"Je accountgebruikersnaam voor authenticatie\"],\"+yPBXI\":[\"Bestand kiezen\"],\"+zy2Nq\":[\"Type\"],\"/09cao\":[\"Lage verbindingsbeveiliging (niveau \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"Markeer jezelf als terug\"],\"/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\"],\"/xQ19T\":[\"Bots op dit netwerk\"],\"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\"],\"1C/fOn\":[\"De bot heeft nog geen slash-opdrachten geregistreerd.\"],\"1DSr1i\":[\"Registreren voor een account\"],\"1O/24y\":[\"Kanaallijst aan/uit\"],\"1QfxQT\":[\"Sluiten\"],\"1VPJJ2\":[\"Waarschuwing externe link\"],\"1ZC/dv\":[\"Geen ongelezen vermeldingen of berichten\"],\"1pO1zi\":[\"Servernaam is vereist\"],\"1t/NnN\":[\"Weigeren\"],\"1uwfzQ\":[\"Kanaalonderwerp bekijken\"],\"268g7c\":[\"Weergavenaam invoeren\"],\"2F9+AZ\":[\"Nog geen ruw IRC-verkeer vastgelegd. Probeer verbinding te maken of een bericht te versturen.\"],\"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\"],\"2xXP/g\":[\"Meedoen aan een kanaal\"],\"2ygf/L\":[\"← Terug\"],\"2zEgxj\":[\"GIF's zoeken...\"],\"3JjdaA\":[\"Uitvoeren\"],\"3NJ4MW\":[\"Heropen de workflow die dit bericht heeft gemaakt (\",[\"stepCount\"],\" stappen)\"],\"3RdPhl\":[\"Kanaal hernoemen\"],\"3THokf\":[\"Gebruiker met voice\"],\"3TSz9S\":[\"Minimaliseren\"],\"3et0TM\":[\"Scroll de chat naar dit antwoord\"],\"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\"],\"4uKgKr\":[\"UIT\"],\"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\":[\"Sleep bestanden hierheen 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:\"],\"9q17ZR\":[[\"0\"],\" is verplicht.\"],\"9qIYMn\":[\"Nieuwe bijnaam\"],\"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?\"],\"BIDT9R\":[\"Bots\"],\"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\"],\"CaQ1Gb\":[\"Config-gedefinieerde bot. Bewerk obbyircd.conf en /REHASH om de status te wijzigen.\"],\"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\"],\"DSHF2K\":[\"De workflow die dit bericht heeft gemaakt, bevindt zich niet meer in de status\"],\"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...\"],\"Eu7YKa\":[\"zelf-geregistreerd\"],\"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: 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...\"],\"IcHxhR\":[\"offline\"],\"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\"],\"JMXMCX\":[\"Afwezigheidsbericht\"],\"JXGkhG\":[\"Kanaalnaam wijzigen (alleen operators)\"],\"JYiL1b\":[\"een van:\"],\"JcD7qf\":[\"Meer acties\"],\"JdkA+c\":[\"Geheim (+s)\"],\"Jmu12l\":[\"Serverkanalen\"],\"JvQ++s\":[\"Markdown inschakelen\"],\"K2jwh/\":[\"Geen WHOIS-gegevens beschikbaar\"],\"K4vEhk\":[\"(opgeschort)\"],\"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\"],\"KtADxr\":[\"voerde uit\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" stap(pen) wachten op goedkeuring\"],\"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\\\")\"],\"LYzbQ2\":[\"Hulpmiddel\"],\"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\"],\"M45wtf\":[\"Deze opdracht heeft geen parameters.\"],\"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\"],\"N40H+G\":[\"Alle\"],\"N7TQbE\":[\"Gebruiker uitnodigen voor \",[\"channelName\"]],\"NCca/o\":[\"Voer standaard bijnaam in...\"],\"NQN2HS\":[\"Opschorting opheffen\"],\"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+HhhG\":[\"Fluister naar een gebruiker in de context van het huidige kanaal\"],\"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\"]],\"RlCInP\":[\"Slash-opdrachten\"],\"RySp6q\":[\"Reacties verbergen\"],\"RzfkXn\":[\"Wijzig je bijnaam op deze server\"],\"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\"],\"TImSWn\":[\"(afgehandeld door ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Markeer jezelf als afwezig\"],\"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!\"],\"UqtiKk\":[\"Automatisch sluiten over \",[\"secondsLeft\"],\"s\"],\"UrEy4W\":[\"Open een privébericht naar een gebruiker\"],\"UtUIRh\":[[\"0\"],\" oudere berichten\"],\"UwzP+U\":[\"Beveiligde verbinding\"],\"V0/A4O\":[\"Kanaaleigenaar\"],\"V2dwib\":[[\"0\"],\" moet een getal zijn.\"],\"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\"],\"XklovM\":[\"Bezig…\"],\"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+tK3n\":[\"Eerste bericht om te versturen\"],\"Y/qryO\":[\"Geen gebruikers gevonden die overeenkomen met je zoekopdracht\"],\"YAqRpI\":[\"Accountregistratie voor \",[\"account\"],\" geslaagd: \",[\"message\"]],\"YBXJ7j\":[\"IN\"],\"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:\"],\"Z7ZXbT\":[\"Goedkeuren\"],\"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\"],\"ZqLD8l\":[\"Serverbreed\"],\"a2/8e5\":[\"Onderwerp ingesteld na (min geleden)\"],\"aHKcKc\":[\"Vorige pagina\"],\"aJTbXX\":[\"Oper-wachtwoord\"],\"aP9gNu\":[\"uitvoer afgekapt\"],\"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\"],\"bFDO8z\":[\"gateway online\"],\"bQ6BJn\":[\"Stel gedetailleerde floodbeveiligingsregels in. Elke regel bepaalt welk type activiteit wordt bewaakt en welke actie wordt ondernomen als drempelwaarden worden overschreden.\"],\"bVBC/W\":[\"Gateway verbonden\"],\"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\"],\"cde3ce\":[\"Bericht aan <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\"],\"dRqrdL\":[[\"0\"],\" moet een geheel getal zijn.\"],\"dXqxlh\":[\"<0>⚠️ Beveiligingsrisico! Deze verbinding kan kwetsbaar zijn voor onderschepping of man-in-the-middle-aanvallen.\"],\"da9Q/R\":[\"Kanaalmodi gewijzigd\"],\"dhJN3N\":[\"Reacties tonen\"],\"dj2xTE\":[\"Melding sluiten\"],\"dnUmOX\":[\"Nog geen bots geregistreerd op dit netwerk.\"],\"dpCzmC\":[\"Floodbeveiligingsinstellingen\"],\"e7KzRG\":[[\"0\"],\" stap(pen)\"],\"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: 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.\"],\"fYdEvu\":[\"Workflowgeschiedenis (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Selecteer een bot links om de opdrachten en beheeracties te zien.\"],\"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\"],\"jUXib7\":[\"Het antwoordbericht is niet meer in beeld\"],\"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:\"],\"kG2fiE\":[\"config-gedefinieerd\"],\"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.\"],\"ky2mw7\":[\"via @\",[\"0\"]],\"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\"],\"lH+ed1\":[\"Wachten op eerste stap…\"],\"lHy8N5\":[\"Meer kanalen laden...\"],\"lasgrr\":[\"gebruikt\"],\"lbpf14\":[\"Deelnemen aan \",[\"value\"]],\"lf3MT4\":[\"Kanaal om te verlaten (standaard het huidige)\"],\"lfFsZ4\":[\"Kanalen\"],\"lkNdiH\":[\"Accountnaam\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Afbeelding uploaden\"],\"loQxaJ\":[\"Ik ben terug\"],\"lvfaxv\":[\"START\"],\"m16xKo\":[\"Toevoegen\"],\"m8flAk\":[\"Voorbeeld (nog niet geüpload)\"],\"mDkV0w\":[\"Workflow starten…\"],\"mEPxTp\":[\"<0>⚠️ Wees voorzichtig! Open alleen links van vertrouwde bronnen. Kwaadaardige links kunnen je beveiliging of privacy in gevaar brengen.\"],\"mHGdhG\":[\"Serverinformatie\"],\"mHS8lb\":[\"Bericht in #\",[\"0\"]],\"mHfd/S\":[\"Wat je aan het doen bent\"],\"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\"],\"nX4XLG\":[\"Operatoracties\"],\"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\"],\"oaTtrx\":[\"Bots zoeken\"],\"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\"],\"pQBYsE\":[\"Beantwoord in chat\"],\"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\"],\"qXgujk\":[\"Stuur een actie / emote\"],\"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\"],\" heeft de volgende beveiligingsproblemen:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"is nu bekend als **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" heeft je uitgenodigd om deel te nemen aan \",[\"channel\"]],\"sW5OjU\":[\"verplicht\"],\"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\"],\"thC9Rq\":[\"Een kanaal verlaten\"],\"tiBsJk\":[\"heeft \",[\"channelName\"],\" verlaten\"],\"tt4/UD\":[\"verliet de server (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Kanaal om mee te doen (#naam)\"],\"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\"]],\"uMIUx8\":[\"Bot \",[\"0\"],\" verwijderen? Dit verwijdert de databaserij zacht; hergebruik de nick later pas na een /REHASH.\"],\"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\"],\"wCKe3+\":[\"Workflowgeschiedenis\"],\"wFjjxZ\":[\"werd gekickt uit \",[\"channelName\"],\" door \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Geen uitzonderingen op bannen gevonden\"],\"wPrGnM\":[\"Kanaalbeheerder\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Redenering\"],\"wbm86v\":[\"Weergeven wanneer gebruikers kanalen betreden of verlaten\"],\"wdxz7K\":[\"Bron\"],\"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/invitation-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\"],\"xeiujy\":[\"Tekst\"],\"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\"]],\"zPBDzU\":[\"Workflow annuleren\"],\"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..b7916d6d 100644 --- a/src/locales/nl/messages.po +++ b/src/locales/nl/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - IRC naar de toekomst brengen" msgid "— open in viewer" msgstr "— openen in viewer" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(afgehandeld door ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(opgeschort)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Toon 1 item meer} other {Toon {1} items meer}}" msgid "{0} and {1} are typing..." msgstr "{0} en {1} zijn aan het typen..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} is verplicht." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} typt..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} moet een getal zijn." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} moet een geheel getal zijn." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} oudere berichten" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} stap(pen)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} stap(pen) wachten op goedkeuring" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Geavanceerde filters" msgid "Album" msgstr "Album" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Alle" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Alle inhoud" @@ -297,6 +334,12 @@ msgstr "Filters toepassen en vernieuwen" msgid "Applying..." msgstr "Toepassen..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Goedkeuren" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Geverifieerd account" msgid "Auto Fallback to Single Line" msgstr "Automatisch terugvallen op één regel" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Automatisch sluiten over {secondsLeft}s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Automatisch als operator authenticeren bij verbinden" @@ -359,6 +406,10 @@ msgstr "Afwezig" msgid "Away from keyboard" msgstr "Niet achter het toetsenbord" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Afwezigheidsbericht" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Afwezigheidsbericht" msgid "Away:" msgstr "Afwezig:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Bannen" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "De bot heeft nog geen slash-opdrachten geregistreerd." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Bots" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Bots op dit netwerk" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Alle kanalen op de server bekijken" @@ -430,6 +499,7 @@ msgstr "Alle kanalen op de server bekijken" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Verbinding annuleren" msgid "Cancel reply" msgstr "Antwoord annuleren" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Workflow annuleren" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Kanaalnaam wijzigen (alleen operators)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Wijzig je bijnaam op deze server" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Kanaalmodi gewijzigd" @@ -459,6 +537,7 @@ msgstr "Nickname gewijzigd" msgid "changed the topic to: {topic}" msgstr "heeft het onderwerp gewijzigd naar: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Kanaal" @@ -519,6 +598,14 @@ msgstr "Kanaaleigenaar" msgid "Channel Settings" msgstr "Kanaalinstellingen" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Kanaal om mee te doen (#naam)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Kanaal om te verlaten (standaard het huidige)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Kanaalonderwerp" @@ -531,6 +618,7 @@ msgstr "Kanaal verschijnt niet in LIST- of NAMES-opdrachten" msgid "channel-name" msgstr "kanaalnaam" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Kanalen" @@ -606,6 +694,9 @@ msgstr "Clientlimiet (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Reacties" msgid "Comments ({commentCount})" msgstr "Reacties ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "config-gedefinieerd" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Config-gedefinieerde bot. Bewerk obbyircd.conf en /REHASH om de status te wijzigen." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "Stel gedetailleerde floodbeveiligingsregels in. Elke regel bepaalt welk type activiteit wordt bewaakt en welke actie wordt ondernomen als drempelwaarden worden overschreden." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Standaard nickname" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Verwijderen" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Bot {0} verwijderen? Dit verwijdert de databaserij zacht; hergebruik de nick later pas na een /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Kanaal verwijderen" @@ -843,6 +948,10 @@ msgstr "Ontdekken" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Ontdek de wereld van IRC met ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Sluiten" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Melding sluiten" @@ -891,7 +1000,7 @@ msgstr "Downloaden" #: src/components/layout/ChatArea.tsx msgid "Drop files to upload" -msgstr "Bestanden hier neerzetten om te uploaden" +msgstr "Sleep bestanden hierheen om te uploaden" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Kanalen filteren..." msgid "Filter members…" msgstr "Leden filteren…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Eerste bericht om te versturen" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Floodprofiel (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (globale ban)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway verbonden" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway online" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Algemeen" @@ -1186,6 +1307,10 @@ msgstr "Afbeelding {0} van {1}" msgid "Image preview" msgstr "Afbeeldingsvoorbeeldweergave" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "IN" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1270,6 +1395,10 @@ msgstr "Deelnemen aan {0}" msgid "Join {value}" msgstr "Deelnemen aan {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Meedoen aan een kanaal" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Meer informatie over aangepaste regels →" msgid "Learn more about profiles →" msgstr "Meer informatie over profielen →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Een kanaal verlaten" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Kanaal verlaten" @@ -1411,6 +1544,14 @@ msgstr "Je profielgegevens en metadata beheren" msgid "Mark as bot - usually 'on' or empty" msgstr "Markeren als bot — gewoonlijk 'aan' of leeg" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Markeer jezelf als afwezig" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Markeer jezelf als terug" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Max. gebruikers" @@ -1573,6 +1714,10 @@ msgstr "Netwerknaam" msgid "New DM" msgstr "Nieuw DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Nieuwe bijnaam" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Volgende afbeelding" @@ -1617,6 +1762,10 @@ msgstr "Geen uitzonderingen op bannen gevonden" msgid "No bans found" msgstr "Geen bannen gevonden" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Nog geen bots geregistreerd op dit netwerk." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Geen centrale server:" @@ -1672,7 +1821,7 @@ msgstr "Er worden geen mediavoorbeeldweergaven geladen." #: src/components/ui/RawLogViewer.tsx msgid "No raw IRC traffic captured yet. Try connecting or sending a message." -msgstr "Nog geen ruw IRC-verkeer vastgelegd. Probeer verbinding te maken of een bericht te sturen." +msgstr "Nog geen ruw IRC-verkeer vastgelegd. Probeer verbinding te maken of een bericht te versturen." #: src/components/ui/QuickActions.tsx msgid "No results found" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC — IRC de toekomst in" msgid "Off" msgstr "Uit" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "offline" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Offline" @@ -1754,6 +1908,10 @@ msgstr "Offline" msgid "on" msgstr "aan" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "een van:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Oeps! Netwerksplitsing! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Open een privébericht naar een gebruiker" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Kanaelconfiguratie-instellingen openen" @@ -1828,10 +1990,23 @@ msgstr "Oper-wachtwoord" msgid "Oper Username" msgstr "Oper-gebruikersnaam" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Operatoracties" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Optionele crashrapporten om de app te verbeteren" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "UIT" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "uitvoer afgekapt" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Eigenaar" @@ -1994,6 +2169,10 @@ msgstr "Afsluitbericht" msgid "Quit the server" msgstr "De server verlaten" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "voerde uit" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Reageren" @@ -2024,6 +2203,10 @@ msgstr "Reden" msgid "Reason (optional)" msgstr "Reden (optioneel)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Redenering" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Opnieuw verbinden met server" @@ -2037,6 +2220,11 @@ msgstr "Vernieuwen" msgid "Register for an account" msgstr "Registreren voor een account" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Weigeren" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Ontspannen" @@ -2077,11 +2265,27 @@ msgstr "Dit kanaal op de server hernoemen. Alle gebruikers zien de nieuwe naam." msgid "Render markdown formatting in messages" msgstr "Markdown-opmaak in berichten weergeven" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Heropen de workflow die dit bericht heeft gemaakt ({stepCount} stappen)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Beantwoorden" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "verplicht" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Beantwoord in chat" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Het antwoordbericht is niet meer in beeld" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Opnieuw verzenden" msgid "Rules" msgstr "Regels" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Uitvoeren" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Veilig" @@ -2121,6 +2329,10 @@ msgstr "Opgeslagen" msgid "Saving..." msgstr "Opslaan..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Scroll de chat naar dit antwoord" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Naar beneden scrollen" msgid "Search" msgstr "Zoeken" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Bots zoeken" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Zoek naar GIF's om te beginnen" @@ -2183,6 +2399,10 @@ msgstr "Beveiligingswaarschuwing" msgid "Seek" msgstr "Spoelen" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Selecteer een bot links om de opdrachten en beheeracties te zien." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Selecteer een kanaal" @@ -2195,6 +2415,10 @@ msgstr "Lid selecteren" msgid "Select or add a channel to get started." msgstr "Selecteer of voeg een kanaal toe om te beginnen." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "zelf-geregistreerd" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Een GIF verzenden" msgid "Send a warning message to {username}" msgstr "Een waarschuwingsbericht sturen naar {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Stuur een actie / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Uitnodiging verzenden" @@ -2280,6 +2508,10 @@ msgstr "Serverwachtwoord" msgid "Server-to-server communication may use unencrypted connections" msgstr "Server-naar-servercommunicatie kan niet-versleutelde verbindingen gebruiken" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "Serverbreed" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Een onderwerp instellen…" @@ -2376,6 +2608,10 @@ msgstr "Toont media van de vertrouwde bestandshost van je server. Er worden geen msgid "Signed On" msgstr "Aangemeld op" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Slash-opdrachten" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Software:" @@ -2392,10 +2628,18 @@ msgstr "Sorteren op gebruikers" msgid "SoundCloud player" msgstr "SoundCloud-speler" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Bron" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Privébericht starten" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Workflow starten…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Statusberichten" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Stoppen" @@ -2419,10 +2664,18 @@ msgstr "Streng" msgid "Strict - More aggressive protection" msgstr "Streng — Agressievere beveiliging" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Opschorten" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Systeem" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Tekst" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Tekstkanalen" @@ -2447,6 +2700,14 @@ msgstr "Het onderwerp dat voor dit kanaal wordt weergegeven. Alle gebruikers kun msgid "The user will receive an invitation to join {channelName}." msgstr "De gebruiker ontvangt een uitnodiging om deel te nemen aan {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "De workflow die dit bericht heeft gemaakt, bevindt zich niet meer in de status" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Deze opdracht heeft geen parameters." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Dit veld is vereist" @@ -2510,6 +2771,10 @@ msgstr "Meldingen aan/uit" msgid "Toggle search" msgstr "Zoeken aan/uit" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Hulpmiddel" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Onderwerp ingesteld na (min geleden)" @@ -2527,6 +2792,10 @@ msgstr "Onderwerp:" msgid "Total: {0}" msgstr "Totaal: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Transport" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Vertrouwde bronnen" @@ -2561,6 +2830,10 @@ msgstr "Privégesprek losmaken" msgid "Unpin this private message conversation" msgstr "Dit privéberichtgesprek losmaken" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Opschorting opheffen" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Uploaden" @@ -2687,6 +2960,11 @@ msgstr "Zeer ontspannen" msgid "Very Strict" msgstr "Zeer streng" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "via @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2730,6 +3008,10 @@ msgstr "Gebruiker met voice" msgid "Volume" msgstr "Volume" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Wachten op eerste stap…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Uit het kanaal verwijderd" msgid "We don't store your IRC communications on our servers" msgstr "We slaan je IRC-communicatie niet op onze servers op" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Welkom bij {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Wat ben je aan het doen?" msgid "What this means:" msgstr "Wat dit betekent:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Wat je aan het doen bent" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "Waar denk je aan?" @@ -2780,6 +3070,10 @@ msgstr "Waar denk je aan?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Fluister naar een gebruiker in de context van het huidige kanaal" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Breed — Ruimere beveiligingsscope" @@ -2788,6 +3082,19 @@ msgstr "Breed — Ruimere beveiligingsscope" msgid "Will default to 'no reason' if left empty" msgstr "Standaard 'geen reden' als leeggelaten" +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Workflowgeschiedenis" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Workflowgeschiedenis ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Bezig…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/pl/messages.mjs b/src/locales/pl/messages.mjs index b4c322a9..a4a23a28 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: 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\"],\"\"],\"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! 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: 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! 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\"],\" 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/invitationnie 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\"],\"+fRR7i\":[\"Zawieś\"],\"+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\"],\")\"],\"/0mUpR\":[\"Oznacz siebie jako obecnego\"],\"/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\"],\"/xQ19T\":[\"Boty w tej sieci\"],\"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ę\"],\"1C/fOn\":[\"Bot nie zarejestrował jeszcze żadnych poleceń ukośnikowych.\"],\"1DSr1i\":[\"Zarejestruj konto\"],\"1O/24y\":[\"Przełącz listę kanałów\"],\"1QfxQT\":[\"Odrzuć\"],\"1VPJJ2\":[\"Ostrzeżenie o zewnętrznym linku\"],\"1ZC/dv\":[\"Brak nieprzeczytanych wzmianek lub wiadomości\"],\"1pO1zi\":[\"Nazwa serwera jest wymagana\"],\"1t/NnN\":[\"Odrzuć\"],\"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\"],\"2xXP/g\":[\"Dołącz do kanału\"],\"2ygf/L\":[\"← Wróć\"],\"2zEgxj\":[\"Szukaj GIFów...\"],\"3JjdaA\":[\"Uruchom\"],\"3NJ4MW\":[\"Otwórz ponownie przepływ pracy, który wygenerował tę wiadomość (\",[\"stepCount\"],\" kroków)\"],\"3RdPhl\":[\"Zmień nazwę kanału\"],\"3THokf\":[\"Użytkownik z głosem\"],\"3TSz9S\":[\"Minimalizuj\"],\"3et0TM\":[\"Przewiń czat do tej odpowiedzi\"],\"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\"],\"4uKgKr\":[\"WYJŚCIE\"],\"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 je 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:\"],\"9q17ZR\":[[\"0\"],\" jest wymagane.\"],\"9qIYMn\":[\"Nowy pseudonim\"],\"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?\"],\"BIDT9R\":[\"Boty\"],\"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\"],\"CaQ1Gb\":[\"Bot zdefiniowany w konfiguracji. Edytuj obbyircd.conf i wykonaj /REHASH, aby zmienić stan.\"],\"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ń\"],\"DSHF2K\":[\"Przepływ pracy, który wygenerował tę wiadomość, nie jest już w stanie\"],\"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...\"],\"Eu7YKa\":[\"samodzielnie zarejestrowany\"],\"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: 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ą...\"],\"IcHxhR\":[\"offline\"],\"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\"],\"JMXMCX\":[\"Wiadomość o nieobecności\"],\"JXGkhG\":[\"Zmień nazwę kanału (tylko dla operatorów)\"],\"JYiL1b\":[\"jedno z:\"],\"JcD7qf\":[\"Więcej akcji\"],\"JdkA+c\":[\"Tajny (+s)\"],\"Jmu12l\":[\"Kanały serwera\"],\"JvQ++s\":[\"Włącz Markdown\"],\"K2jwh/\":[\"Brak dostępnych danych WHOIS\"],\"K4vEhk\":[\"(zawieszony)\"],\"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\"],\"KtADxr\":[\"uruchomił(a)\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" kroków oczekuje na zatwierdzenie\"],\"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\\\")\"],\"LYzbQ2\":[\"Narzędzie\"],\"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ść\"],\"M45wtf\":[\"To polecenie nie przyjmuje parametrów.\"],\"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\"],\"N40H+G\":[\"Wszystkie\"],\"N7TQbE\":[\"Zaproś użytkownika do \",[\"channelName\"]],\"NCca/o\":[\"Wprowadź domyślny pseudonim...\"],\"NQN2HS\":[\"Wznów\"],\"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+HhhG\":[\"Szepnij do użytkownika w kontekście bieżącego kanału\"],\"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\"]],\"RlCInP\":[\"Polecenia ukośnikowe\"],\"RySp6q\":[\"Ukryj komentarze\"],\"RzfkXn\":[\"Zmień swój pseudonim na tym serwerze\"],\"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\"],\"TImSWn\":[\"(obsługiwane przez ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Oznacz siebie jako nieobecnego\"],\"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!\"],\"UqtiKk\":[\"Automatyczne zamknięcie za \",[\"secondsLeft\"],\"s\"],\"UrEy4W\":[\"Otwórz prywatną wiadomość do użytkownika\"],\"UtUIRh\":[[\"0\"],\" starszych wiadomości\"],\"UwzP+U\":[\"Bezpieczne połączenie\"],\"V0/A4O\":[\"Właściciel kanału\"],\"V2dwib\":[[\"0\"],\" musi być liczbą.\"],\"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ę\"],\"XklovM\":[\"Praca w toku…\"],\"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+tK3n\":[\"Pierwsza wiadomość do wysłania\"],\"Y/qryO\":[\"Nie znaleziono użytkowników pasujących do wyszukiwania\"],\"YAqRpI\":[\"Rejestracja konta \",[\"account\"],\" powiodła się: \",[\"message\"]],\"YBXJ7j\":[\"WEJŚCIE\"],\"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:\"],\"Z7ZXbT\":[\"Zatwierdź\"],\"ZJSWfw\":[\"Wiadomość wyświetlana przy rozłączeniu z serwera\"],\"ZR1dJ4\":[\"Zaproszenia\"],\"ZdWg0V\":[\"Otwórz w przeglądarce\"],\"ZhRBbl\":[\"Szukaj wiadomości…\"],\"Zmcu3y\":[\"Zaawansowane filtry\"],\"ZqLD8l\":[\"Dla całego serwera\"],\"a2/8e5\":[\"Temat ustawiony po (min temu)\"],\"aHKcKc\":[\"Poprzednia strona\"],\"aJTbXX\":[\"Hasło operatora\"],\"aP9gNu\":[\"wynik skrócony\"],\"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\"],\"bFDO8z\":[\"gateway online\"],\"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.\"],\"bVBC/W\":[\"Gateway połączony\"],\"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ę\"],\"cde3ce\":[\"Wiadomość do <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\"],\"dRqrdL\":[[\"0\"],\" musi być liczbą całkowitą.\"],\"dXqxlh\":[\"<0>⚠️ Zagrożenie bezpieczeństwa! 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\"],\"dnUmOX\":[\"Żaden bot nie został jeszcze zarejestrowany w tej sieci.\"],\"dpCzmC\":[\"Ustawienia ochrony przed floodem\"],\"e7KzRG\":[[\"0\"],\" kroków\"],\"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: 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.\"],\"fYdEvu\":[\"Historia przepływów pracy (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Wybierz bota po lewej stronie, aby zobaczyć jego polecenia i akcje zarządzania.\"],\"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\"],\"jUXib7\":[\"Wiadomość z odpowiedzią nie jest już widoczna\"],\"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:\"],\"kG2fiE\":[\"zdefiniowany w konfiguracji\"],\"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ąć.\"],\"ky2mw7\":[\"przez @\",[\"0\"]],\"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ż\"],\"lH+ed1\":[\"Oczekiwanie na pierwszy krok…\"],\"lHy8N5\":[\"Wczytywanie kolejnych kanałów...\"],\"lasgrr\":[\"użyte\"],\"lbpf14\":[\"Dołącz do \",[\"value\"]],\"lf3MT4\":[\"Kanał do opuszczenia (domyślnie bieżący)\"],\"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)\"],\"mDkV0w\":[\"Uruchamianie przepływu pracy…\"],\"mEPxTp\":[\"<0>⚠️ Uwaga! 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\"]],\"mHfd/S\":[\"Co robisz\"],\"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\"],\"nX4XLG\":[\"Akcje operatora\"],\"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\"],\"oaTtrx\":[\"Szukaj botów\"],\"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\"],\"pQBYsE\":[\"Odpowiedziano na czacie\"],\"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\"],\"qXgujk\":[\"Wyślij akcję / emotkę\"],\"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\"],\" 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\"]],\"sW5OjU\":[\"wymagane\"],\"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\"],\"thC9Rq\":[\"Opuść kanał\"],\"tiBsJk\":[\"opuścił \",[\"channelName\"]],\"tt4/UD\":[\"wyszedł (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Kanał do dołączenia (#nazwa)\"],\"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\"]],\"uMIUx8\":[\"Usunąć bota \",[\"0\"],\"? Powoduje to miękkie usunięcie wiersza bazy danych; ponowne użycie nicka będzie możliwe dopiero po /REHASH.\"],\"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ść\"],\"wCKe3+\":[\"Historia przepływów pracy\"],\"wFjjxZ\":[\"został wyrzucony z \",[\"channelName\"],\" przez \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nie znaleziono wyjątków od bana\"],\"wPrGnM\":[\"Administrator kanału\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Rozumowanie\"],\"wbm86v\":[\"Wyświetlaj gdy użytkownicy dołączają lub opuszczają kanały\"],\"wdxz7K\":[\"Źródło\"],\"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/invitationnie 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\"],\"xeiujy\":[\"Tekst\"],\"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\"]],\"zPBDzU\":[\"Anuluj przepływ pracy\"],\"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..c97a7a24 100644 --- a/src/locales/pl/messages.po +++ b/src/locales/pl/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Przenosimy IRC w przyszłość" msgid "— open in viewer" msgstr "— otwórz w przeglądarce" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(obsługiwane przez ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(zawieszony)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Pokaż 1 więcej element} few {Pokaż {1} więcej eleme msgid "{0} and {1} are typing..." msgstr "{0} i {1} piszą..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} jest wymagane." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} pisze..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} musi być liczbą." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} musi być liczbą całkowitą." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} starszych wiadomości" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} kroków" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} kroków oczekuje na zatwierdzenie" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Zaawansowane filtry" msgid "Album" msgstr "Album" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Wszystkie" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Wszystkie treści" @@ -297,6 +334,12 @@ msgstr "Zastosuj filtry i odśwież" msgid "Applying..." msgstr "Stosowanie..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Zatwierdź" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Uwierzytelnione konto" msgid "Auto Fallback to Single Line" msgstr "Automatyczny powrót do jednej linii" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Automatyczne zamknięcie za {secondsLeft}s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Automatycznie uwierzytelniaj jako operator przy połączeniu" @@ -359,6 +406,10 @@ msgstr "Nieobecny" msgid "Away from keyboard" msgstr "Z dala od klawiatury" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Wiadomość o nieobecności" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Wiadomość o nieobecności" msgid "Away:" msgstr "Nieobecny:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Blokady" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Bot nie zarejestrował jeszcze żadnych poleceń ukośnikowych." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Boty" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Boty w tej sieci" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Przeglądaj wszystkie kanały na serwerze" @@ -430,6 +499,7 @@ msgstr "Przeglądaj wszystkie kanały na serwerze" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Anuluj połączenie" msgid "Cancel reply" msgstr "Anuluj odpowiedź" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Anuluj przepływ pracy" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Zmień nazwę kanału (tylko dla operatorów)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Zmień swój pseudonim na tym serwerze" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Zmieniono tryby kanału" @@ -459,6 +537,7 @@ msgstr "Zmieniono nick" msgid "changed the topic to: {topic}" msgstr "zmienił temat na: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Kanał" @@ -519,6 +598,14 @@ msgstr "Właściciel kanału" msgid "Channel Settings" msgstr "Ustawienia kanału" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Kanał do dołączenia (#nazwa)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Kanał do opuszczenia (domyślnie bieżący)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Temat kanału" @@ -531,6 +618,7 @@ msgstr "Kanał nie będzie widoczny w poleceniach LIST ani NAMES" msgid "channel-name" msgstr "nazwa-kanału" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Kanały" @@ -606,6 +694,9 @@ msgstr "Limit klientów (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Komentarze" msgid "Comments ({commentCount})" msgstr "Komentarze ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "zdefiniowany w konfiguracji" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Bot zdefiniowany w konfiguracji. Edytuj obbyircd.conf i wykonaj /REHASH, aby zmienić stan." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "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." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Domyślny nick" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Usuń" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Usunąć bota {0}? Powoduje to miękkie usunięcie wiersza bazy danych; ponowne użycie nicka będzie możliwe dopiero po /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Usuń kanał" @@ -843,6 +948,10 @@ msgstr "Odkryj" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Odkryj świat IRC z ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Odrzuć" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Odrzuć powiadomienie" @@ -891,7 +1000,7 @@ msgstr "Pobierz" #: src/components/layout/ChatArea.tsx msgid "Drop files to upload" -msgstr "Upuść pliki, aby przesłać" +msgstr "Upuść pliki, aby je przesłać" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Filtruj kanały..." msgid "Filter members…" msgstr "Filtruj członków…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Pierwsza wiadomość do wysłania" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Profil floodu (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (globalny ban)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway połączony" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway online" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Ogólne" @@ -1186,6 +1307,10 @@ msgstr "Obraz {0} z {1}" msgid "Image preview" msgstr "Podgląd obrazu" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "WEJŚCIE" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1270,6 +1395,10 @@ msgstr "Dołącz do {0}" msgid "Join {value}" msgstr "Dołącz do {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Dołącz do kanału" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Dowiedz się więcej o niestandardowych regułach →" msgid "Learn more about profiles →" msgstr "Dowiedz się więcej o profilach →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Opuść kanał" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Opuść kanał" @@ -1411,6 +1544,14 @@ msgstr "Zarządzaj informacjami w profilu i metadanymi" msgid "Mark as bot - usually 'on' or empty" msgstr "Oznacz jako bota – zazwyczaj 'on' lub puste" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Oznacz siebie jako nieobecnego" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Oznacz siebie jako obecnego" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Maks. użytkowników" @@ -1573,6 +1714,10 @@ msgstr "Nazwa sieci" msgid "New DM" msgstr "Nowy DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Nowy pseudonim" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Następny obraz" @@ -1617,6 +1762,10 @@ msgstr "Nie znaleziono wyjątków od bana" msgid "No bans found" msgstr "Nie znaleziono żadnych banów" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Żaden bot nie został jeszcze zarejestrowany w tej sieci." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Brak centralnego serwera:" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC – IRC w nowoczesnym wydaniu" msgid "Off" msgstr "Wyłączone" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "offline" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Offline" @@ -1754,6 +1908,10 @@ msgstr "Offline" msgid "on" msgstr "wł" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "jedno z:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Ups! Podział sieci! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Otwórz prywatną wiadomość do użytkownika" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Otwórz ustawienia konfiguracji kanału" @@ -1828,10 +1990,23 @@ msgstr "Hasło operatora" msgid "Oper Username" msgstr "Nazwa użytkownika operatora" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Akcje operatora" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Opcjonalne raporty o błędach w celu ulepszenia aplikacji" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "WYJŚCIE" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "wynik skrócony" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Właściciel" @@ -1994,6 +2169,10 @@ msgstr "Wiadomość pożegnalna" msgid "Quit the server" msgstr "Opuścił serwer" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "uruchomił(a)" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Zareaguj" @@ -2024,6 +2203,10 @@ msgstr "Powód" msgid "Reason (optional)" msgstr "Powód (opcjonalnie)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Rozumowanie" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Ponownie połącz z serwerem" @@ -2037,6 +2220,11 @@ msgstr "Odśwież" msgid "Register for an account" msgstr "Zarejestruj konto" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Odrzuć" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Łagodny" @@ -2077,11 +2265,27 @@ msgstr "Zmień nazwę tego kanału na serwerze. Wszyscy użytkownicy zobaczą no msgid "Render markdown formatting in messages" msgstr "Renderuj formatowanie Markdown w wiadomościach" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Otwórz ponownie przepływ pracy, który wygenerował tę wiadomość ({stepCount} kroków)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Odpowiedz" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "wymagane" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Odpowiedziano na czacie" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Wiadomość z odpowiedzią nie jest już widoczna" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Spróbuj wysłać ponownie" msgid "Rules" msgstr "Reguły" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Uruchom" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Bezpieczne" @@ -2121,6 +2329,10 @@ msgstr "Zapisano" msgid "Saving..." msgstr "Zapisywanie..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Przewiń czat do tej odpowiedzi" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Przewiń na dół" msgid "Search" msgstr "Szukaj" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Szukaj botów" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Wyszukaj GIFy, aby rozpocząć" @@ -2183,6 +2399,10 @@ msgstr "Ostrzeżenie o bezpieczeństwie" msgid "Seek" msgstr "Przewijaj" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Wybierz bota po lewej stronie, aby zobaczyć jego polecenia i akcje zarządzania." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Wybierz kanał" @@ -2195,6 +2415,10 @@ msgstr "Wybierz członka" msgid "Select or add a channel to get started." msgstr "Wybierz lub dodaj kanał, aby zacząć." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "samodzielnie zarejestrowany" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Wyślij GIF" msgid "Send a warning message to {username}" msgstr "Wyślij wiadomość ostrzegawczą do {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Wyślij akcję / emotkę" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Wyślij zaproszenie" @@ -2280,6 +2508,10 @@ msgstr "Hasło serwera" msgid "Server-to-server communication may use unencrypted connections" msgstr "Komunikacja między serwerami może używać nieszyfrowanych połączeń" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "Dla całego serwera" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Ustaw temat…" @@ -2376,6 +2608,10 @@ msgstr "Wyświetla media z zaufanego hosta plików Twojego serwera. Żadne żąd msgid "Signed On" msgstr "Zalogowany od" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Polecenia ukośnikowe" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Oprogramowanie:" @@ -2392,10 +2628,18 @@ msgstr "Sortuj według użytkowników" msgid "SoundCloud player" msgstr "Odtwarzacz SoundCloud" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Źródło" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Rozpocznij prywatną rozmowę" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Uruchamianie przepływu pracy…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Wiadomości statusu" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Zatrzymaj" @@ -2419,10 +2664,18 @@ msgstr "Rygorystyczny" msgid "Strict - More aggressive protection" msgstr "Rygorystyczny – bardziej agresywna ochrona" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Zawieś" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "System" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Tekst" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Kanały tekstowe" @@ -2447,6 +2700,14 @@ msgstr "Temat, który będzie wyświetlany dla tego kanału. Wszyscy użytkownic msgid "The user will receive an invitation to join {channelName}." msgstr "Użytkownik otrzyma zaproszenie do dołączenia do {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Przepływ pracy, który wygenerował tę wiadomość, nie jest już w stanie" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "To polecenie nie przyjmuje parametrów." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "To pole jest wymagane" @@ -2510,6 +2771,10 @@ msgstr "Przełącz powiadomienia" msgid "Toggle search" msgstr "Przełącz wyszukiwanie" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Narzędzie" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Temat ustawiony po (min temu)" @@ -2527,6 +2792,10 @@ msgstr "Temat:" msgid "Total: {0}" msgstr "Łącznie: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Transport" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Zaufane źródła" @@ -2561,6 +2830,10 @@ msgstr "Odepnij prywatną rozmowę" msgid "Unpin this private message conversation" msgstr "Odepnij tę prywatną rozmowę" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Wznów" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Prześlij" @@ -2687,6 +2960,11 @@ msgstr "Bardzo łagodny" msgid "Very Strict" msgstr "Bardzo rygorystyczny" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "przez @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Wideo" @@ -2730,6 +3008,10 @@ msgstr "Użytkownik z głosem" msgid "Volume" msgstr "Głośność" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Oczekiwanie na pierwszy krok…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Został wyrzucony z kanału" msgid "We don't store your IRC communications on our servers" msgstr "Nie przechowujemy Twoich komunikatów IRC na naszych serwerach" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Witamy na {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Co teraz robisz?" msgid "What this means:" msgstr "Co to oznacza:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Co robisz" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "Co masz na myśli?" @@ -2780,6 +3070,10 @@ msgstr "Co masz na myśli?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Szepnij do użytkownika w kontekście bieżącego kanału" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Szeroki – szerszy zakres ochrony" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Historia przepływów pracy" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Historia przepływów pracy ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Praca w toku…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/pt/messages.mjs b/src/locales/pt/messages.mjs index 938ee647..2fc3d870 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: 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\"],\"\"],\"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! 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: 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! 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\"],\" 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/invitationnã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\"],\"+fRR7i\":[\"Suspender\"],\"+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\"],\")\"],\"/0mUpR\":[\"Marcar-se como de volta\"],\"/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\"],\"/xQ19T\":[\"Bots nesta rede\"],\"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\"],\"1C/fOn\":[\"O bot ainda não registrou nenhum comando slash.\"],\"1DSr1i\":[\"Registrar uma conta\"],\"1O/24y\":[\"Alternar Lista de Canais\"],\"1QfxQT\":[\"Dispensar\"],\"1VPJJ2\":[\"Aviso de Link Externo\"],\"1ZC/dv\":[\"Nenhuma menção ou mensagem não lida\"],\"1pO1zi\":[\"Nome do servidor é obrigatório\"],\"1t/NnN\":[\"Rejeitar\"],\"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\"],\"2xXP/g\":[\"Entrar em um canal\"],\"2ygf/L\":[\"← Voltar\"],\"2zEgxj\":[\"Pesquisar GIFs...\"],\"3JjdaA\":[\"Executar\"],\"3NJ4MW\":[\"Reabrir o workflow que produziu esta mensagem (\",[\"stepCount\"],\" etapas)\"],\"3RdPhl\":[\"Renomear Canal\"],\"3THokf\":[\"Usuário com voz\"],\"3TSz9S\":[\"Minimizar\"],\"3et0TM\":[\"Rolar o chat até esta resposta\"],\"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\"],\"4uKgKr\":[\"OUT\"],\"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:\"],\"9q17ZR\":[[\"0\"],\" é obrigatório.\"],\"9qIYMn\":[\"Novo apelido\"],\"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?\"],\"BIDT9R\":[\"Bots\"],\"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\"],\"CaQ1Gb\":[\"Bot definido na configuração. Edite obbyircd.conf e use /REHASH para alterar o estado.\"],\"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\"],\"DSHF2K\":[\"O workflow que produziu esta mensagem não está mais no estado\"],\"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...\"],\"Eu7YKa\":[\"auto-registrado\"],\"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: 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...\"],\"IcHxhR\":[\"offline\"],\"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\"],\"JMXMCX\":[\"Mensagem de ausência\"],\"JXGkhG\":[\"Alterar o nome do canal (apenas operadores)\"],\"JYiL1b\":[\"um de:\"],\"JcD7qf\":[\"Mais ações\"],\"JdkA+c\":[\"Secreto (+s)\"],\"Jmu12l\":[\"Canais do Servidor\"],\"JvQ++s\":[\"Ativar Markdown\"],\"K2jwh/\":[\"Nenhum dado WHOIS disponível\"],\"K4vEhk\":[\"(suspenso)\"],\"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\"],\"KtADxr\":[\"executou\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" etapa(s) aguardando aprovação\"],\"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\\\")\"],\"LYzbQ2\":[\"Ferramenta\"],\"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\"],\"M45wtf\":[\"Este comando não recebe parâmetros.\"],\"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\"],\"N40H+G\":[\"Todos\"],\"N7TQbE\":[\"Convidar usuário para \",[\"channelName\"]],\"NCca/o\":[\"Inserir apelido padrão...\"],\"NQN2HS\":[\"Reativar\"],\"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+HhhG\":[\"Sussurrar para um usuário no contexto do canal atual\"],\"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\"]],\"RlCInP\":[\"Comandos slash\"],\"RySp6q\":[\"Ocultar comentários\"],\"RzfkXn\":[\"Alterar seu apelido neste servidor\"],\"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\"],\"TImSWn\":[\"(gerenciado pelo ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Marcar-se como ausente\"],\"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!\"],\"UqtiKk\":[\"Dispensar automaticamente em \",[\"secondsLeft\"],\"s\"],\"UrEy4W\":[\"Abrir uma mensagem privada para um usuário\"],\"UtUIRh\":[[\"0\"],\" mensagens antigas\"],\"UwzP+U\":[\"Conexão segura\"],\"V0/A4O\":[\"Proprietário do canal\"],\"V2dwib\":[[\"0\"],\" deve ser um número.\"],\"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\"],\"XklovM\":[\"Trabalhando…\"],\"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+tK3n\":[\"Primeira mensagem a enviar\"],\"Y/qryO\":[\"Nenhum usuário encontrado para sua pesquisa\"],\"YAqRpI\":[\"Registro da conta \",[\"account\"],\" bem-sucedido: \",[\"message\"]],\"YBXJ7j\":[\"IN\"],\"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:\"],\"Z7ZXbT\":[\"Aprovar\"],\"ZJSWfw\":[\"Mensagem exibida ao desconectar do servidor\"],\"ZR1dJ4\":[\"Convites\"],\"ZdWg0V\":[\"Abrir no navegador\"],\"ZhRBbl\":[\"Pesquisar mensagens…\"],\"Zmcu3y\":[\"Filtros avançados\"],\"ZqLD8l\":[\"Em todo o servidor\"],\"a2/8e5\":[\"Tópico definido após (min atrás)\"],\"aHKcKc\":[\"Página anterior\"],\"aJTbXX\":[\"Senha Oper\"],\"aP9gNu\":[\"saída truncada\"],\"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\"],\"bFDO8z\":[\"gateway online\"],\"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.\"],\"bVBC/W\":[\"Gateway conectado\"],\"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\"],\"cde3ce\":[\"Mensagem <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\"],\"dRqrdL\":[[\"0\"],\" deve ser um número inteiro.\"],\"dXqxlh\":[\"<0>⚠️ Risco de Segurança! 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\"],\"dnUmOX\":[\"Nenhum bot registrado nesta rede ainda.\"],\"dpCzmC\":[\"Configurações de proteção contra flood\"],\"e7KzRG\":[[\"0\"],\" etapa(s)\"],\"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: 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.\"],\"fYdEvu\":[\"Histórico de workflow (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Selecione um bot à esquerda para ver seus comandos e ações de gerenciamento.\"],\"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\"],\"jUXib7\":[\"A mensagem de resposta não está mais visível\"],\"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:\"],\"kG2fiE\":[\"definido na config\"],\"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.\"],\"ky2mw7\":[\"via @\",[\"0\"]],\"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\"],\"lH+ed1\":[\"Aguardando a primeira etapa…\"],\"lHy8N5\":[\"Carregando mais canais...\"],\"lasgrr\":[\"usado\"],\"lbpf14\":[\"Entrar em \",[\"value\"]],\"lf3MT4\":[\"Canal para sair (padrão é o atual)\"],\"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)\"],\"mDkV0w\":[\"Iniciando workflow…\"],\"mEPxTp\":[\"<0>⚠️ Cuidado! Abra apenas links de fontes confiáveis. Links maliciosos podem comprometer sua segurança ou privacidade.\"],\"mHGdhG\":[\"Informações do servidor\"],\"mHS8lb\":[\"Mensagem #\",[\"0\"]],\"mHfd/S\":[\"O que você está fazendo\"],\"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\"],\"nX4XLG\":[\"Ações de operador\"],\"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\"],\"oaTtrx\":[\"Pesquisar bots\"],\"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\"],\"pQBYsE\":[\"Respondido no chat\"],\"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\"],\"qXgujk\":[\"Enviar uma ação / emote\"],\"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\"],\" apresenta as seguintes preocupações de segurança:\"],\"sGH11W\":[\"Servidor\"],\"sHI1H+\":[\"agora é conhecido como **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" convidou você para entrar em \",[\"channel\"]],\"sW5OjU\":[\"obrigatório\"],\"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\"],\"thC9Rq\":[\"Sair de um canal\"],\"tiBsJk\":[\"saiu de \",[\"channelName\"]],\"tt4/UD\":[\"saiu (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Canal para entrar (#nome)\"],\"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\"]],\"uMIUx8\":[\"Excluir o bot \",[\"0\"],\"? Isto faz a exclusão lógica da linha do banco de dados; reutilize o nick mais tarde apenas após um /REHASH.\"],\"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\"],\"wCKe3+\":[\"Histórico de workflow\"],\"wFjjxZ\":[\"foi expulso de \",[\"channelName\"],\" por \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nenhuma exceção de banimento encontrada\"],\"wPrGnM\":[\"Administrador do canal\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Raciocínio\"],\"wbm86v\":[\"Exibir quando usuários entram ou saem de canais\"],\"wdxz7K\":[\"Origem\"],\"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/invitationnã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\"],\"xeiujy\":[\"Texto\"],\"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\"]],\"zPBDzU\":[\"Cancelar workflow\"],\"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..eb077232 100644 --- a/src/locales/pt/messages.po +++ b/src/locales/pt/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Levando o IRC para o futuro" msgid "— open in viewer" msgstr "— abrir no visualizador" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(gerenciado pelo ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(suspenso)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Mostrar 1 item a mais} other {Mostrar {1} itens a mais} msgid "{0} and {1} are typing..." msgstr "{0} e {1} estão digitando..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} é obrigatório." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} está digitando..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} deve ser um número." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} deve ser um número inteiro." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} mensagens antigas" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} etapa(s)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} etapa(s) aguardando aprovação" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Filtros avançados" msgid "Album" msgstr "Álbum" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Todos" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Todo o Conteúdo" @@ -297,6 +334,12 @@ msgstr "Aplicar filtros e atualizar" msgid "Applying..." msgstr "Aplicando..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Aprovar" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Conta autenticada" msgid "Auto Fallback to Single Line" msgstr "Retorno automático para linha única" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Dispensar automaticamente em {secondsLeft}s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Autenticar automaticamente como operador ao conectar" @@ -359,6 +406,10 @@ msgstr "Ausente" msgid "Away from keyboard" msgstr "Longe do teclado" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Mensagem de ausência" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Mensagem de ausência" msgid "Away:" msgstr "Ausente:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Banimentos" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "O bot ainda não registrou nenhum comando slash." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Bots" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Bots nesta rede" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Navegar por todos os canais do servidor" @@ -430,6 +499,7 @@ msgstr "Navegar por todos os canais do servidor" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Cancelar Conexão" msgid "Cancel reply" msgstr "Cancelar resposta" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Cancelar workflow" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Alterar o nome do canal (apenas operadores)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Alterar seu apelido neste servidor" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Modos do canal alterados" @@ -459,6 +537,7 @@ msgstr "Apelido alterado" msgid "changed the topic to: {topic}" msgstr "alterou o tópico para: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Canal" @@ -519,6 +598,14 @@ msgstr "Proprietário do canal" msgid "Channel Settings" msgstr "Configurações do Canal" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Canal para entrar (#nome)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Canal para sair (padrão é o atual)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Tópico do canal" @@ -531,6 +618,7 @@ msgstr "O canal não aparecerá nos comandos LIST ou NAMES" msgid "channel-name" msgstr "nome-do-canal" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Canais" @@ -606,6 +694,9 @@ msgstr "Limite de clientes (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Comentários" msgid "Comments ({commentCount})" msgstr "Comentários ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "definido na config" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Bot definido na configuração. Edite obbyircd.conf e use /REHASH para alterar o estado." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "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." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Apelido padrão" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Excluir" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Excluir o bot {0}? Isto faz a exclusão lógica da linha do banco de dados; reutilize o nick mais tarde apenas após um /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Excluir Canal" @@ -843,6 +948,10 @@ msgstr "Descobrir" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Descubra o mundo do IRC com o ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Dispensar" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Dispensar notificação" @@ -1039,6 +1148,10 @@ msgstr "Filtrar canais..." msgid "Filter members…" msgstr "Filtrar membros…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Primeira mensagem a enviar" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Perfil de flood (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (ban global)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway conectado" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway online" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Geral" @@ -1186,6 +1307,10 @@ msgstr "Imagem {0} de {1}" msgid "Image preview" msgstr "Visualização da imagem" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "IN" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1270,6 +1395,10 @@ msgstr "Entrar em {0}" msgid "Join {value}" msgstr "Entrar em {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Entrar em um canal" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Saiba mais sobre regras personalizadas →" msgid "Learn more about profiles →" msgstr "Saiba mais sobre perfis →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Sair de um canal" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Sair do canal" @@ -1411,6 +1544,14 @@ msgstr "Gerenciar suas informações de perfil e metadados" msgid "Mark as bot - usually 'on' or empty" msgstr "Marcar como bot, geralmente 'on' ou vazio" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Marcar-se como ausente" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Marcar-se como de volta" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Usuários máx." @@ -1573,6 +1714,10 @@ msgstr "Nome da Rede" msgid "New DM" msgstr "Nova mensagem direta" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Novo apelido" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Próxima imagem" @@ -1617,6 +1762,10 @@ msgstr "Nenhuma exceção de banimento encontrada" msgid "No bans found" msgstr "Nenhum banimento encontrado" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Nenhum bot registrado nesta rede ainda." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Sem servidor central:" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - Levando o IRC ao futuro" msgid "Off" msgstr "Desligado" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "offline" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Offline" @@ -1754,6 +1908,10 @@ msgstr "Offline" msgid "on" msgstr "ativo" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "um de:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Ops! Divisão de rede! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Abrir uma mensagem privada para um usuário" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Abrir configurações do canal" @@ -1828,10 +1990,23 @@ msgstr "Senha Oper" msgid "Oper Username" msgstr "Nome de usuário Oper" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Ações de operador" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Relatórios de falha opcionais para melhorar o app" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "OUT" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "saída truncada" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Dono" @@ -1994,6 +2169,10 @@ msgstr "Mensagem de saída" msgid "Quit the server" msgstr "Saiu do servidor" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "executou" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Reagir" @@ -2024,6 +2203,10 @@ msgstr "Motivo" msgid "Reason (optional)" msgstr "Motivo (opcional)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Raciocínio" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Reconectar ao servidor" @@ -2037,6 +2220,11 @@ msgstr "Atualizar" msgid "Register for an account" msgstr "Registrar uma conta" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Rejeitar" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Relaxado" @@ -2077,11 +2265,27 @@ msgstr "Renomear este canal no servidor. Todos os usuários verão o novo nome." msgid "Render markdown formatting in messages" msgstr "Renderizar formatação Markdown nas mensagens" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Reabrir o workflow que produziu esta mensagem ({stepCount} etapas)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Responder" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "obrigatório" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Respondido no chat" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "A mensagem de resposta não está mais visível" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Tentar enviar novamente" msgid "Rules" msgstr "Regras" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Executar" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Seguro" @@ -2121,6 +2329,10 @@ msgstr "Salvo" msgid "Saving..." msgstr "Salvando..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Rolar o chat até esta resposta" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Rolar para o fim" msgid "Search" msgstr "Pesquisar" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Pesquisar bots" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Pesquise GIFs para começar" @@ -2183,6 +2399,10 @@ msgstr "Aviso de Segurança" msgid "Seek" msgstr "Avançar" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Selecione um bot à esquerda para ver seus comandos e ações de gerenciamento." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Selecionar um canal" @@ -2195,6 +2415,10 @@ msgstr "Selecionar Membro" msgid "Select or add a channel to get started." msgstr "Selecione ou adicione um canal para começar." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "auto-registrado" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Enviar um GIF" msgid "Send a warning message to {username}" msgstr "Enviar uma mensagem de aviso para {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Enviar uma ação / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Enviar convite" @@ -2280,6 +2508,10 @@ msgstr "Senha do Servidor" msgid "Server-to-server communication may use unencrypted connections" msgstr "A comunicação servidor a servidor pode usar conexões não criptografadas" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "Em todo o servidor" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Definir um tópico…" @@ -2376,6 +2608,10 @@ msgstr "Exibe mídia do host de arquivos confiável do seu servidor. Nenhuma req msgid "Signed On" msgstr "Conectado em" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Comandos slash" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Software:" @@ -2392,10 +2628,18 @@ msgstr "Ordenar por usuários" msgid "SoundCloud player" msgstr "Player do SoundCloud" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Origem" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Iniciar Mensagem Privada" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Iniciando workflow…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Mensagens de Status" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Parar" @@ -2419,10 +2664,18 @@ msgstr "Estrito" msgid "Strict - More aggressive protection" msgstr "Estrito – Proteção mais agressiva" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Suspender" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Sistema" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Texto" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Canais de texto" @@ -2447,6 +2700,14 @@ msgstr "O tópico exibido para este canal. Todos os usuários podem ver." msgid "The user will receive an invitation to join {channelName}." msgstr "O usuário receberá um convite para entrar em {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "O workflow que produziu esta mensagem não está mais no estado" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Este comando não recebe parâmetros." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Este campo é obrigatório" @@ -2510,6 +2771,10 @@ msgstr "Alternar Notificações" msgid "Toggle search" msgstr "Alternar pesquisa" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Ferramenta" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Tópico definido após (min atrás)" @@ -2527,6 +2792,10 @@ msgstr "Tópico:" msgid "Total: {0}" msgstr "Total: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Transporte" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Fontes Confiáveis" @@ -2561,6 +2830,10 @@ msgstr "Desafixar Conversa Privada" msgid "Unpin this private message conversation" msgstr "Desafixar esta conversa de mensagem privada" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Reativar" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Carregar" @@ -2687,6 +2960,11 @@ msgstr "Muito Relaxado" msgid "Very Strict" msgstr "Muito Estrito" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "via @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Vídeo" @@ -2730,6 +3008,10 @@ msgstr "Usuário com voz" msgid "Volume" msgstr "Volume" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Aguardando a primeira etapa…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Foi expulso do canal" msgid "We don't store your IRC communications on our servers" msgstr "Não armazenamos suas comunicações IRC em nossos servidores" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Bem-vindo ao {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "O que você está fazendo?" msgid "What this means:" msgstr "O que isso significa:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "O que você está fazendo" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "O que você está pensando?" @@ -2780,6 +3070,10 @@ msgstr "O que você está pensando?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Sussurrar para um usuário no contexto do canal atual" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Amplo – Escopo de proteção mais amplo" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Histórico de workflow" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Histórico de workflow ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Trabalhando…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/ro/messages.mjs b/src/locales/ro/messages.mjs index 675078c6..8d21d584 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: 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\"],\"\"],\"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! 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: 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! 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\"],\" 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/invitationnu 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\"],\"+fRR7i\":[\"Suspendă\"],\"+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\"],\")\"],\"/0mUpR\":[\"Marchează-te ca revenit\"],\"/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\"],\"/xQ19T\":[\"Boți pe această rețea\"],\"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ă\"],\"1C/fOn\":[\"Botul nu a înregistrat încă nicio comandă slash.\"],\"1DSr1i\":[\"Înregistrează un cont\"],\"1O/24y\":[\"Comută lista de canale\"],\"1QfxQT\":[\"Închide\"],\"1VPJJ2\":[\"Avertisment link extern\"],\"1ZC/dv\":[\"Nicio mențiune sau mesaj necitit\"],\"1pO1zi\":[\"Numele serverului este obligatoriu\"],\"1t/NnN\":[\"Respinge\"],\"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\"],\"2xXP/g\":[\"Intră pe un canal\"],\"2ygf/L\":[\"← Înapoi\"],\"2zEgxj\":[\"Caută GIF-uri...\"],\"3JjdaA\":[\"Execută\"],\"3NJ4MW\":[\"Redeschide fluxul de lucru care a produs acest mesaj (\",[\"stepCount\"],\" pași)\"],\"3RdPhl\":[\"Redenumește canalul\"],\"3THokf\":[\"Utilizator cu drept de vorbire\"],\"3TSz9S\":[\"Minimizează\"],\"3et0TM\":[\"Derulează chatul la acest răspuns\"],\"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\"],\"4uKgKr\":[\"IEȘIRE\"],\"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\":[\"Plasați 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:\"],\"9q17ZR\":[[\"0\"],\" este obligatoriu.\"],\"9qIYMn\":[\"Pseudonim nou\"],\"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?\"],\"BIDT9R\":[\"Boți\"],\"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\"],\"CaQ1Gb\":[\"Bot definit în configurare. Editează obbyircd.conf și folosește /REHASH pentru a schimba starea.\"],\"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\"],\"DSHF2K\":[\"Fluxul de lucru care a produs acest mesaj nu mai este în stare\"],\"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...\"],\"Eu7YKa\":[\"autoînregistrat\"],\"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: 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...\"],\"IcHxhR\":[\"offline\"],\"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ă\"],\"JMXMCX\":[\"Mesaj de absență\"],\"JXGkhG\":[\"Schimbă numele canalului (numai operatori)\"],\"JYiL1b\":[\"unul dintre:\"],\"JcD7qf\":[\"Mai multe acțiuni\"],\"JdkA+c\":[\"Secret (+s)\"],\"Jmu12l\":[\"Canale server\"],\"JvQ++s\":[\"Activați Markdown\"],\"K2jwh/\":[\"Nu există date WHOIS disponibile\"],\"K4vEhk\":[\"(suspendat)\"],\"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\"],\"KtADxr\":[\"a executat\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" pas(i) în așteptarea aprobării\"],\"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”)\"],\"LYzbQ2\":[\"Instrument\"],\"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\"],\"M45wtf\":[\"Această comandă nu acceptă parametri.\"],\"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\"],\"N40H+G\":[\"Toate\"],\"N7TQbE\":[\"Invitați utilizatorul în \",[\"channelName\"]],\"NCca/o\":[\"Introduceți porecla implicită...\"],\"NQN2HS\":[\"Anulează suspendarea\"],\"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+HhhG\":[\"Șoptește unui utilizator în contextul canalului curent\"],\"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\"]],\"RlCInP\":[\"Comenzi slash\"],\"RySp6q\":[\"Ascundeți comentariile\"],\"RzfkXn\":[\"Schimbă-ți pseudonimul pe acest server\"],\"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\"],\"TImSWn\":[\"(gestionat de ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Marchează-te ca absent\"],\"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!\"],\"UqtiKk\":[\"Se închide automat în \",[\"secondsLeft\"],\"s\"],\"UrEy4W\":[\"Deschide un mesaj privat către un utilizator\"],\"UtUIRh\":[[\"0\"],\" mesaje mai vechi\"],\"UwzP+U\":[\"Conexiune securizată\"],\"V0/A4O\":[\"Proprietar de canal\"],\"V2dwib\":[[\"0\"],\" trebuie să fie un număr.\"],\"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\"],\"XklovM\":[\"Se lucrează…\"],\"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+tK3n\":[\"Primul mesaj de trimis\"],\"Y/qryO\":[\"Niciun utilizator găsit care să corespundă căutării\"],\"YAqRpI\":[\"Înregistrarea contului \",[\"account\"],\" a reușit: \",[\"message\"]],\"YBXJ7j\":[\"INTRARE\"],\"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:\"],\"Z7ZXbT\":[\"Aprobă\"],\"ZJSWfw\":[\"Mesaj afișat la deconectarea de la server\"],\"ZR1dJ4\":[\"Invitații\"],\"ZdWg0V\":[\"Deschide în browser\"],\"ZhRBbl\":[\"Caută mesaje…\"],\"Zmcu3y\":[\"Filtre avansate\"],\"ZqLD8l\":[\"La nivel de server\"],\"a2/8e5\":[\"Subiect setat după (min în urmă)\"],\"aHKcKc\":[\"Pagina anterioară\"],\"aJTbXX\":[\"Parolă oper\"],\"aP9gNu\":[\"ieșire trunchiată\"],\"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\"],\"bFDO8z\":[\"gateway online\"],\"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.\"],\"bVBC/W\":[\"Gateway conectat\"],\"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ă\"],\"cde3ce\":[\"Mesaj <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\"],\"dRqrdL\":[[\"0\"],\" trebuie să fie un număr întreg.\"],\"dXqxlh\":[\"<0>⚠️ Risc de securitate! 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\"],\"dnUmOX\":[\"Niciun bot înregistrat pe această rețea încă.\"],\"dpCzmC\":[\"Setări de protecție flood\"],\"e7KzRG\":[[\"0\"],\" pas(i)\"],\"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: 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.\"],\"fYdEvu\":[\"Istoric fluxuri de lucru (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Selectează un bot din stânga pentru a-i vedea comenzile și acțiunile de administrare.\"],\"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\"],\"jUXib7\":[\"Mesajul de răspuns nu mai este vizibil\"],\"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:\"],\"kG2fiE\":[\"definit în configurare\"],\"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.\"],\"ky2mw7\":[\"prin @\",[\"0\"]],\"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ă\"],\"lH+ed1\":[\"Se așteaptă primul pas…\"],\"lHy8N5\":[\"Se încarcă mai multe canale...\"],\"lasgrr\":[\"utilizat\"],\"lbpf14\":[\"Intrați în \",[\"value\"]],\"lf3MT4\":[\"Canal de părăsit (implicit cel curent)\"],\"lfFsZ4\":[\"Canale\"],\"lkNdiH\":[\"Nume cont\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Încarcă imagine\"],\"loQxaJ\":[\"M-am întors\"],\"lvfaxv\":[\"ACASĂ\"],\"m16xKo\":[\"Adaugă\"],\"m8flAk\":[\"Previzualizare (neîncărcat încă)\"],\"mDkV0w\":[\"Se pornește fluxul de lucru…\"],\"mEPxTp\":[\"<0>⚠️ Atenție! Deschide numai linkuri din surse de încredere. Linkurile malițioase îți pot compromite securitatea sau confidențialitatea.\"],\"mHGdhG\":[\"Informații server\"],\"mHS8lb\":[\"Mesaj #\",[\"0\"]],\"mHfd/S\":[\"Ce faci\"],\"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\"],\"nX4XLG\":[\"Acțiuni de operator\"],\"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\"],\"oaTtrx\":[\"Caută boți\"],\"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\"],\"pQBYsE\":[\"A răspuns în chat\"],\"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ă\"],\"qXgujk\":[\"Trimite o acțiune / emote\"],\"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\"],\" 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\"]],\"sW5OjU\":[\"obligatoriu\"],\"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ă\"],\"thC9Rq\":[\"Părăsește un canal\"],\"tiBsJk\":[\"a părăsit \",[\"channelName\"]],\"tt4/UD\":[\"a ieșit (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Canal de intrat (#nume)\"],\"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\"]],\"uMIUx8\":[\"Ștergi botul \",[\"0\"],\"? Aceasta marchează rândul din baza de date ca șters; reutilizează nick-ul mai târziu doar după un /REHASH.\"],\"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ă\"],\"wCKe3+\":[\"Istoric fluxuri de lucru\"],\"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\"],\"wUf2OL\":[\"Raționament\"],\"wbm86v\":[\"Afișează când utilizatorii intră sau ies din canale\"],\"wdxz7K\":[\"Sursă\"],\"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/invitationnu 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\"],\"xeiujy\":[\"Text\"],\"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\"]],\"zPBDzU\":[\"Anulează fluxul de lucru\"],\"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..b3427ab9 100644 --- a/src/locales/ro/messages.po +++ b/src/locales/ro/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Aducem IRC în viitor" msgid "— open in viewer" msgstr "— deschide în vizualizator" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(gestionat de ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(suspendat)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Arată 1 element în plus} few {Arată {1} elemente în msgid "{0} and {1} are typing..." msgstr "{0} și {1} scriu..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} este obligatoriu." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} scrie..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} trebuie să fie un număr." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} trebuie să fie un număr întreg." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} mesaje mai vechi" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} pas(i)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} pas(i) în așteptarea aprobării" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Filtre avansate" msgid "Album" msgstr "Album" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Toate" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Tot conținutul" @@ -297,6 +334,12 @@ msgstr "Aplică filtrele și actualizează" msgid "Applying..." msgstr "Se aplică..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Aprobă" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Cont autentificat" msgid "Auto Fallback to Single Line" msgstr "Revenire automată la o singură linie" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Se închide automat în {secondsLeft}s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Autentificare automată ca operator la conectare" @@ -359,6 +406,10 @@ msgstr "Absent" msgid "Away from keyboard" msgstr "Departe de tastatură" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Mesaj de absență" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Mesaj de absență" msgid "Away:" msgstr "Absent:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Banuri" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Botul nu a înregistrat încă nicio comandă slash." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Boți" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Boți pe această rețea" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Răsfoiește toate canalele de pe server" @@ -430,6 +499,7 @@ msgstr "Răsfoiește toate canalele de pe server" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Anulează conexiunea" msgid "Cancel reply" msgstr "Anulează răspunsul" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Anulează fluxul de lucru" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Schimbă numele canalului (numai operatori)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Schimbă-ți pseudonimul pe acest server" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Moduri canal modificate" @@ -459,6 +537,7 @@ msgstr "Poreclă schimbată" msgid "changed the topic to: {topic}" msgstr "a schimbat subiectul la: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Canal" @@ -519,6 +598,14 @@ msgstr "Proprietar de canal" msgid "Channel Settings" msgstr "Setări canal" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Canal de intrat (#nume)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Canal de părăsit (implicit cel curent)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Subiect canal" @@ -531,6 +618,7 @@ msgstr "Canalul nu va apărea în comenzile LIST sau NAMES" msgid "channel-name" msgstr "nume-canal" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Canale" @@ -606,6 +694,9 @@ msgstr "Limită clienți (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Comentarii" msgid "Comments ({commentCount})" msgstr "Comentarii ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "definit în configurare" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Bot definit în configurare. Editează obbyircd.conf și folosește /REHASH pentru a schimba starea." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "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." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Pseudonim implicit" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Șterge" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Ștergi botul {0}? Aceasta marchează rândul din baza de date ca șters; reutilizează nick-ul mai târziu doar după un /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Șterge canal" @@ -843,6 +948,10 @@ msgstr "Descoperă" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Descoperă lumea IRC cu ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Închide" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Respinge notificarea" @@ -891,7 +1000,7 @@ msgstr "Descarcă" #: src/components/layout/ChatArea.tsx msgid "Drop files to upload" -msgstr "Plasează fișierele pentru a le încărca" +msgstr "Plasați fișierele pentru a le încărca" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Filtrează canale..." msgid "Filter members…" msgstr "Filtrează membri…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Primul mesaj de trimis" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Profil flood (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (ban global)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway conectat" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway online" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "General" @@ -1186,6 +1307,10 @@ msgstr "Imaginea {0} din {1}" msgid "Image preview" msgstr "Previzualizare imagine" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "INTRARE" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1270,6 +1395,10 @@ msgstr "Alătură-te la {0}" msgid "Join {value}" msgstr "Intrați în {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Intră pe un canal" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Aflați mai multe despre regulile personalizate →" msgid "Learn more about profiles →" msgstr "Aflați mai multe despre profiluri →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Părăsește un canal" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Părăsește canalul" @@ -1411,6 +1544,14 @@ msgstr "Gestionează informațiile profilului și metadatele" msgid "Mark as bot - usually 'on' or empty" msgstr "Marcați ca bot, de obicei 'on' sau gol" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Marchează-te ca absent" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Marchează-te ca revenit" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Utilizatori max." @@ -1573,6 +1714,10 @@ msgstr "Nume rețea" msgid "New DM" msgstr "Mesaj direct nou" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Pseudonim nou" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Imaginea următoare" @@ -1617,6 +1762,10 @@ msgstr "Nu s-au găsit excepții de banare" msgid "No bans found" msgstr "Nu s-au găsit banuri" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Niciun bot înregistrat pe această rețea încă." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Fără server central:" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - Aducând IRC în viitor" msgid "Off" msgstr "Oprit" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "offline" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Deconectat" @@ -1754,6 +1908,10 @@ msgstr "Deconectat" msgid "on" msgstr "activ" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "unul dintre:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Oops! Rețeaua s-a împărțit! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Deschide un mesaj privat către un utilizator" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Deschide setările de configurare ale canalului" @@ -1828,10 +1990,23 @@ msgstr "Parolă oper" msgid "Oper Username" msgstr "Nume utilizator operator" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Acțiuni de operator" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Rapoarte opționale de erori pentru îmbunătățirea aplicației" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "IEȘIRE" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "ieșire trunchiată" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Proprietar" @@ -1994,6 +2169,10 @@ msgstr "Mesaj de ieșire" msgid "Quit the server" msgstr "A ieșit de pe server" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "a executat" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Reacționează" @@ -2024,6 +2203,10 @@ msgstr "Motiv" msgid "Reason (optional)" msgstr "Motiv (opțional)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Raționament" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Reconectează-te la server" @@ -2037,6 +2220,11 @@ msgstr "Reîmprospătează" msgid "Register for an account" msgstr "Înregistrează un cont" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Respinge" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Relaxat" @@ -2077,11 +2265,27 @@ msgstr "Redenumiți acest canal pe server. Toți utilizatorii vor vedea noul num msgid "Render markdown formatting in messages" msgstr "Afișați formatarea Markdown în mesaje" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Redeschide fluxul de lucru care a produs acest mesaj ({stepCount} pași)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Răspunde" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "obligatoriu" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "A răspuns în chat" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Mesajul de răspuns nu mai este vizibil" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Reîncearcă trimiterea" msgid "Rules" msgstr "Reguli" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Execută" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Sigur" @@ -2121,6 +2329,10 @@ msgstr "Salvat" msgid "Saving..." msgstr "Se salvează..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Derulează chatul la acest răspuns" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Derulează în jos" msgid "Search" msgstr "Caută" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Caută boți" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Căutați GIF-uri pentru a începe" @@ -2183,6 +2399,10 @@ msgstr "Avertisment de securitate" msgid "Seek" msgstr "Derulare" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Selectează un bot din stânga pentru a-i vedea comenzile și acțiunile de administrare." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Selectează un canal" @@ -2195,6 +2415,10 @@ msgstr "Selectează un membru" msgid "Select or add a channel to get started." msgstr "Selectează sau adaugă un canal pentru a începe." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "autoînregistrat" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Trimite un GIF" msgid "Send a warning message to {username}" msgstr "Trimite un mesaj de avertizare către {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Trimite o acțiune / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Trimiteți invitația" @@ -2280,6 +2508,10 @@ msgstr "Parolă server" msgid "Server-to-server communication may use unencrypted connections" msgstr "Comunicarea server-la-server poate folosi conexiuni necriptate" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "La nivel de server" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Setează un subiect…" @@ -2376,6 +2608,10 @@ msgstr "Afișează media de pe gazda de fișiere de încredere a serverului. Nu msgid "Signed On" msgstr "Conectat la" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Comenzi slash" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Software:" @@ -2392,10 +2628,18 @@ msgstr "Sortare după utilizatori" msgid "SoundCloud player" msgstr "Player SoundCloud" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Sursă" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Începe un mesaj privat" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Se pornește fluxul de lucru…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Mesaje de stare" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Oprește" @@ -2419,10 +2664,18 @@ msgstr "Strict" msgid "Strict - More aggressive protection" msgstr "Strict – Protecție mai agresivă" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Suspendă" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Sistem" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Text" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Canale text" @@ -2447,6 +2700,14 @@ msgstr "Subiectul afișat pentru acest canal. Toți utilizatorii îl pot vedea." msgid "The user will receive an invitation to join {channelName}." msgstr "Utilizatorul va primi o invitație să se alăture {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Fluxul de lucru care a produs acest mesaj nu mai este în stare" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Această comandă nu acceptă parametri." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Acest câmp este obligatoriu" @@ -2510,6 +2771,10 @@ msgstr "Comută notificările" msgid "Toggle search" msgstr "Comută căutarea" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Instrument" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Subiect setat după (min în urmă)" @@ -2527,6 +2792,10 @@ msgstr "Subiect:" msgid "Total: {0}" msgstr "Total: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Transport" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Surse de încredere" @@ -2561,6 +2830,10 @@ msgstr "Anulează fixarea conversației private" msgid "Unpin this private message conversation" msgstr "Anulează fixarea acestei conversații private" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Anulează suspendarea" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Încărcați" @@ -2687,6 +2960,11 @@ msgstr "Foarte relaxat" msgid "Very Strict" msgstr "Foarte strict" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "prin @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2730,6 +3008,10 @@ msgstr "Utilizator cu drept de vorbire" msgid "Volume" msgstr "Volum" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Se așteaptă primul pas…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "A fost dat afară din canal" msgid "We don't store your IRC communications on our servers" msgstr "Nu stocăm comunicațiile dvs. IRC pe serverele noastre" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Bine ai venit la {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Ce faci?" msgid "What this means:" msgstr "Ce înseamnă aceasta:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Ce faci" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "La ce te gândești?" @@ -2780,6 +3070,10 @@ msgstr "La ce te gândești?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Șoptește unui utilizator în contextul canalului curent" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Larg – Domeniu de protecție mai amplu" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Istoric fluxuri de lucru" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Istoric fluxuri de lucru ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Se lucrează…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/ru/messages.mjs b/src/locales/ru/messages.mjs index 0f0fe7a3..6cd54296 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>Риск: Конфиденциальная информация (сообщения, личные переписки, данные аутентификации) может быть раскрыта сетевым администраторам или злоумышленникам, находящимся между 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\"],\"\"],\"chQsxg\":[\"Копировать форматированный вывод\"],\"cl/A5J\":[\"Добро пожаловать на \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Удалить\"],\"coPLXT\":[\"Мы не храним ваши IRC-переписки на наших серверах\"],\"crYH/6\":[\"Плеер SoundCloud\"],\"d3sis4\":[\"Добавить сервер\"],\"d9aN5k\":[\"Удалить \",[\"username\"],\" из канала\"],\"dEgA5A\":[\"Отмена\"],\"dGi1We\":[\"Открепить эту личную переписку\"],\"dJVuyC\":[\"покинул \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"кому\"],\"dXqxlh\":[\"<0>⚠️ Угроза безопасности! Это соединение может быть уязвимо для перехвата или атак типа «человек посередине».\"],\"da9Q/R\":[\"Изменил режимы канала\"],\"dhJN3N\":[\"Показать комментарии\"],\"dj2xTE\":[\"Закрыть уведомление\"],\"dpCzmC\":[\"Настройки защиты от флуда\"],\"e9dQpT\":[\"Открыть эту ссылку в новой вкладке?\"],\"ePK91l\":[\"Изменить\"],\"eYBDuB\":[\"Загрузите изображение или укажите URL с необязательной подстановкой \",[\"size\"],\" для динамического масштабирования\"],\"edBbee\":[\"Забанить \",[\"username\"],\" по hostmask (запрещает переподключение с того же IP/хоста)\"],\"ekfzWq\":[\"Настройки пользователя\"],\"elPDWs\":[\"Настройте IRC-клиент под себя\"],\"eu2osY\":[\"<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>⚠️ Будьте осторожны! Открывайте ссылки только из доверенных источников. Вредоносные ссылки могут угрожать вашей безопасности или конфиденциальности.\"],\"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\"],\" имеет следующие проблемы безопасности:\"],\"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/invitationне объявлена). Вы по-прежнему можете общаться в чате как обычно; эта панель предназначена для сетей на базе 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\":[\"Сообщение по умолчанию при переходе в режим отсутствия\"],\"+fRR7i\":[\"Приостановить\"],\"+mVPqU\":[\"Отображать форматирование Markdown в сообщениях\"],\"+vqCJH\":[\"Имя пользователя вашего аккаунта для аутентификации\"],\"+yPBXI\":[\"Выбрать файл\"],\"+zy2Nq\":[\"Тип\"],\"/09cao\":[\"Низкий уровень безопасности соединения (уровень \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"Отметить себя как вернувшегося\"],\"/3BQ4J\":[\"Пользователи вне канала не могут отправлять в него сообщения\"],\"/4C8U0\":[\"Копировать всё\"],\"/6BzZF\":[\"Показать/скрыть список участников\"],\"/AkXyp\":[\"Подтвердить?\"],\"/TNOPk\":[\"Пользователь отсутствует\"],\"/XQgft\":[\"Обзор\"],\"/cF7Rs\":[\"Громкость\"],\"/dqduX\":[\"Следующая страница\"],\"/fc3q4\":[\"Весь контент\"],\"/kISDh\":[\"Включить звуки уведомлений\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Аудио\"],\"/rfkZe\":[\"Воспроизводить звуки для упоминаний и сообщений\"],\"/xQ19T\":[\"Боты в этой сети\"],\"0/0ZGA\":[\"Маска имени канала\"],\"0D6j7U\":[\"Подробнее о пользовательских правилах →\"],\"0XsHcR\":[\"Исключить пользователя\"],\"0ZpE//\":[\"Сортировать по пользователям\"],\"0bEPwz\":[\"Отметиться как отсутствующий\"],\"0dGkPt\":[\"Развернуть список каналов\"],\"0gS7M5\":[\"Отображаемое имя\"],\"0kS+M8\":[\"ПримерНЕТ\"],\"0rgoY7\":[\"Подключайтесь только к выбранным вами серверам\"],\"0wdd7X\":[\"Войти\"],\"0wkVYx\":[\"Личные сообщения\"],\"111uHX\":[\"Предпросмотр ссылки\"],\"196EG4\":[\"Удалить личную переписку\"],\"1C/fOn\":[\"Бот ещё не зарегистрировал ни одной слэш-команды.\"],\"1DSr1i\":[\"Зарегистрировать аккаунт\"],\"1O/24y\":[\"Показать/скрыть список каналов\"],\"1QfxQT\":[\"Закрыть\"],\"1VPJJ2\":[\"Предупреждение о внешней ссылке\"],\"1ZC/dv\":[\"Нет непрочитанных упоминаний или сообщений\"],\"1pO1zi\":[\"Необходимо указать имя сервера\"],\"1t/NnN\":[\"Отклонить\"],\"1uwfzQ\":[\"Просмотреть тему канала\"],\"268g7c\":[\"Введите отображаемое имя\"],\"2F9+AZ\":[\"Сырой IRC-трафик ещё не зафиксирован. Попробуйте подключиться или отправить сообщение.\"],\"2FOFq1\":[\"Операторы серверов сети потенциально могут читать ваши сообщения\"],\"2FYpfJ\":[\"Ещё\"],\"2HF1Y2\":[[\"inviter\"],\" пригласил \",[\"target\"],\" присоединиться к \",[\"channel\"]],\"2I70QL\":[\"Просмотреть информацию профиля пользователя\"],\"2QYdmE\":[\"Пользователи:\"],\"2QpEjG\":[\"вышел\"],\"2YE223\":[\"Сообщение #\",[\"0\"],\" (Enter — новая строка, Shift+Enter — отправить)\"],\"2bimFY\":[\"Использовать пароль сервера\"],\"2iTmdZ\":[\"Локальное хранилище:\"],\"2odkwe\":[\"Строгий — более агрессивная защита\"],\"2uDhbA\":[\"Введите имя пользователя для приглашения\"],\"2xXP/g\":[\"Войти в канал\"],\"2ygf/L\":[\"← Назад\"],\"2zEgxj\":[\"Поиск GIF...\"],\"3JjdaA\":[\"Выполнить\"],\"3NJ4MW\":[\"Снова открыть рабочий процесс, создавший это сообщение (\",[\"stepCount\"],\" шагов)\"],\"3RdPhl\":[\"Переименовать канал\"],\"3THokf\":[\"Пользователь с голосом\"],\"3TSz9S\":[\"Свернуть\"],\"3et0TM\":[\"Прокрутить чат к этому ответу\"],\"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\":[\"Автоматически переключаться на однострочный режим для коротких сообщений\"],\"4uKgKr\":[\"ВЫХОД\"],\"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\":[\"Вопросы о конфиденциальности? Свяжитесь с нами:\"],\"9q17ZR\":[[\"0\"],\" обязателен.\"],\"9qIYMn\":[\"Новый никнейм\"],\"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\":[\"У вас есть несохранённые изменения. Вы уверены, что хотите закрыть без сохранения?\"],\"BIDT9R\":[\"Боты\"],\"BIf9fi\":[\"Ваше статусное сообщение\"],\"BPm98R\":[\"Сервер не выбран. Сначала выберите сервер на боковой панели; пригласительные ссылки управляются отдельно для каждого сервера.\"],\"BZz3md\":[\"Ваш личный сайт\"],\"Bgm/H7\":[\"Разрешить ввод нескольких строк текста\"],\"BiQIl1\":[\"Закрепить эту личную переписку\"],\"BlNZZ2\":[\"Нажмите, чтобы перейти к сообщению\"],\"Bowq3c\":[\"Только операторы могут изменять тему канала\"],\"Btozzp\":[\"Срок действия этого изображения истёк\"],\"Bycfjm\":[\"Всего: \",[\"0\"]],\"C6IBQc\":[\"Копировать весь JSON\"],\"C9L9wL\":[\"Сбор данных\"],\"CDq4wC\":[\"Модерировать пользователя\"],\"CHVRxG\":[\"Сообщение @\",[\"0\"],\" (Shift+Enter — новая строка)\"],\"CN9zdR\":[\"Необходимо указать имя и пароль оператора\"],\"CW3sYa\":[\"Добавить реакцию \",[\"emoji\"]],\"CaAkqd\":[\"Показывать выходы\"],\"CaQ1Gb\":[\"Бот, заданный в конфигурации. Отредактируйте obbyircd.conf и выполните /REHASH, чтобы изменить состояние.\"],\"CbvaYj\":[\"Забанить по никнейму\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Выберите канал\"],\"CsekCi\":[\"Обычный\"],\"D+NlUC\":[\"Система\"],\"D28t6+\":[\"подключился и отключился\"],\"DB8zMK\":[\"Применить\"],\"DBcWHr\":[\"Пользовательский файл звука уведомления\"],\"DSHF2K\":[\"Рабочий процесс, создавший это сообщение, больше не находится в состоянии\"],\"DTy9Xw\":[\"Предпросмотр медиа\"],\"Dj4pSr\":[\"Выберите надёжный пароль\"],\"Du+zn+\":[\"Поиск...\"],\"Du2T2f\":[\"Настройка не найдена\"],\"DwsSVQ\":[\"Применить фильтры и обновить\"],\"E3W/zd\":[\"Никнейм по умолчанию\"],\"E6nRW7\":[\"Копировать URL\"],\"E703RG\":[\"Режимы:\"],\"EAeu1Z\":[\"Отправить приглашение\"],\"EFKJQT\":[\"Настройка\"],\"EGPQBv\":[\"Пользовательские правила флуда (+f)\"],\"ELik0r\":[\"Просмотреть полную политику конфиденциальности\"],\"EPbeC2\":[\"Просмотреть или изменить тему канала\"],\"EQCDNT\":[\"Введите имя пользователя опера...\"],\"EUvulZ\":[\"Найдено 1 сообщение, соответствующее \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Следующее изображение\"],\"EdQY6l\":[\"Нет\"],\"EnqLYU\":[\"Поиск серверов...\"],\"Eu7YKa\":[\"самостоятельно зарегистрирован\"],\"F0OKMc\":[\"Редактировать сервер\"],\"F6Int2\":[\"Включить выделения\"],\"FDoLyE\":[\"Макс. пользователей\"],\"FUU/hZ\":[\"Управляет количеством внешних медиафайлов, загружаемых в чат.\"],\"Fdp03t\":[\"вкл\"],\"FfPWR0\":[\"Диалог\"],\"FjkaiT\":[\"Уменьшить\"],\"FlqOE9\":[\"Что это означает:\"],\"FolHNl\":[\"Управление аккаунтом и аутентификацией\"],\"Fp2Dif\":[\"Покинул сервер\"],\"G5KmCc\":[\"GZ-Line (глобальная Z-Line)\"],\"GDs0lz\":[\"<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\"],\" печатают...\"],\"IcHxhR\":[\"не в сети\"],\"IgrLD/\":[\"Пауза\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Ответить\"],\"IoHMnl\":[\"Максимальное значение: \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Подключение...\"],\"J5T9NW\":[\"Информация о пользователе\"],\"J8Y5+z\":[\"Упс! Разрыв сети! ⚠️\"],\"JBHkBA\":[\"Покинул канал\"],\"JCwL0Q\":[\"Укажите причину (необязательно)\"],\"JFciKP\":[\"Переключить\"],\"JMXMCX\":[\"Сообщение об отсутствии\"],\"JXGkhG\":[\"Изменить имя канала (только для операторов)\"],\"JYiL1b\":[\"один из:\"],\"JcD7qf\":[\"Другие действия\"],\"JdkA+c\":[\"Секретный (+s)\"],\"Jmu12l\":[\"Каналы сервера\"],\"JvQ++s\":[\"Включить Markdown\"],\"K2jwh/\":[\"Данные WHOIS недоступны\"],\"K4vEhk\":[\"(приостановлен)\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Удалить сообщение\"],\"KKBlUU\":[\"Встроить\"],\"KM0pLb\":[\"Добро пожаловать в канал!\"],\"KR6W2h\":[\"Перестать игнорировать пользователя\"],\"KV+Bi1\":[\"Только по приглашению (+i)\"],\"KdCtwE\":[\"Сколько секунд отслеживать флуд-активность до сброса счётчиков\"],\"Kkezga\":[\"Пароль сервера\"],\"KsiQ/8\":[\"Для входа в канал необходимо приглашение\"],\"KtADxr\":[\"выполнил\"],\"L+gB/D\":[\"Информация о канале\"],\"LC1a7n\":[\"IRC-сервер сообщил о низком уровне безопасности межсерверных соединений. Это означает, что при передаче ваших сообщений между IRC-серверами сети они могут быть недостаточно зашифрованы или SSL/TLS-сертификаты могут не проверяться должным образом.\"],\"LN3RO2\":[[\"0\"],\" шаг(ов) ожидает подтверждения\"],\"LNfLR5\":[\"Показывать исключения\"],\"LQb0W/\":[\"Показывать все события\"],\"LU7/yA\":[\"Альтернативное имя для отображения в интерфейсе. Может содержать пробелы, эмодзи и специальные символы. Настоящее имя канала (\",[\"channelName\"],\") по-прежнему будет использоваться для IRC-команд.\"],\"LUb9O7\":[\"Необходимо указать корректный порт сервера\"],\"LV4fT6\":[\"Описание (необязательно, например «Бета-тестеры Q3»)\"],\"LYzbQ2\":[\"Инструмент\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Политика конфиденциальности\"],\"LcuSDR\":[\"Управление информацией профиля и метаданными\"],\"LqLS9B\":[\"Показывать смену никнейма\"],\"LsDQt2\":[\"Настройки канала\"],\"LtI9AS\":[\"Владелец\"],\"LuNhhL\":[\"отреагировал на это сообщение\"],\"M/AZNG\":[\"URL вашего аватара\"],\"M/WIer\":[\"Отправить сообщение\"],\"M45wtf\":[\"Эта команда не принимает параметров.\"],\"M8er/5\":[\"Имя:\"],\"MHk+7g\":[\"Предыдущее изображение\"],\"MRorGe\":[\"Написать в личку\"],\"MVbSGP\":[\"Временное окно (секунды)\"],\"MkpcsT\":[\"Ваши сообщения и настройки хранятся локально на вашем устройстве\"],\"N/hDSy\":[\"Пометить как бота — обычно «on» или пусто\"],\"N40H+G\":[\"Все\"],\"N7TQbE\":[\"Пригласить пользователя в \",[\"channelName\"]],\"NCca/o\":[\"Введите ник по умолчанию...\"],\"NQN2HS\":[\"Возобновить\"],\"Nqs6B9\":[\"Показывает весь внешний медиаконтент. Любой URL может вызвать запрос к неизвестному серверу.\"],\"Nt+9O7\":[\"Использовать WebSocket вместо обычного TCP\"],\"NxIHzc\":[\"Отключить пользователя\"],\"O+HhhG\":[\"Шепнуть пользователю в контексте текущего канала\"],\"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\"]],\"RlCInP\":[\"Слэш-команды\"],\"RySp6q\":[\"Скрыть комментарии\"],\"RzfkXn\":[\"Изменить ваш никнейм на этом сервере\"],\"SPKQTd\":[\"Необходимо указать никнейм\"],\"SPVjfj\":[\"Если оставить пустым, будет использоваться «без причины»\"],\"SQKPvQ\":[\"Пригласить пользователя\"],\"SkZcl+\":[\"Выберите заранее заданный профиль защиты от флуда. Эти профили предоставляют сбалансированные настройки защиты для различных сценариев использования.\"],\"Slr+3C\":[\"Мин. пользователей\"],\"Spnlre\":[\"Вы пригласили \",[\"target\"],\" присоединиться к \",[\"channel\"]],\"T/ckN5\":[\"Открыть в просмотрщике\"],\"T91vKp\":[\"Воспроизвести\"],\"TImSWn\":[\"(обрабатывается ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"TV2Wdu\":[\"Узнайте, как мы обрабатываем ваши данные и защищаем вашу конфиденциальность.\"],\"TgFpwD\":[\"Применяется...\"],\"TkzSFB\":[\"Нет изменений\"],\"TtserG\":[\"Введите настоящее имя\"],\"Ttz9J1\":[\"Введите пароль...\"],\"Tz0i8g\":[\"Настройки\"],\"U3pytU\":[\"Администратор\"],\"U7yg75\":[\"Отметить себя как отошедшего\"],\"UDb2YD\":[\"Реакция\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Вы ещё не создали ни одной пригласительной ссылки. Используйте форму выше, чтобы создать первую.\"],\"UGT5vp\":[\"Сохранить настройки\"],\"UV5hLB\":[\"Баны не найдены\"],\"Uaj3Nd\":[\"Статусные сообщения\"],\"Ue3uny\":[\"По умолчанию (без профиля)\"],\"UkARhe\":[\"Обычный — стандартная защита\"],\"Umn7Cj\":[\"Комментариев пока нет. Будьте первым!\"],\"UqtiKk\":[\"Автоматическое закрытие через \",[\"secondsLeft\"],\"с\"],\"UrEy4W\":[\"Открыть приватное сообщение пользователю\"],\"UtUIRh\":[[\"0\"],\" старых сообщений\"],\"UwzP+U\":[\"Защищённое соединение\"],\"V0/A4O\":[\"Владелец канала\"],\"V2dwib\":[[\"0\"],\" должно быть числом.\"],\"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\":[\"Открепить личный чат\"],\"XklovM\":[\"Выполняется…\"],\"Xm/s+u\":[\"Отображение\"],\"Xp2n93\":[\"Показывает медиафайлы с доверенного файлового хоста вашего сервера. Запросы к внешним сервисам не выполняются.\"],\"XvjC4F\":[\"Сохранение...\"],\"Y+tK3n\":[\"Первое сообщение для отправки\"],\"Y/qryO\":[\"Пользователи по вашему запросу не найдены\"],\"YAqRpI\":[\"Регистрация аккаунта \",[\"account\"],\" успешна: \",[\"message\"]],\"YBXJ7j\":[\"ВХОД\"],\"YEfzvP\":[\"Защищённая тема (+t)\"],\"YQOn6a\":[\"Свернуть список участников\"],\"YRCoE9\":[\"Оператор канала\"],\"YURQaF\":[\"Просмотреть профиль\"],\"YdBSvr\":[\"Управление отображением медиа и внешнего контента\"],\"Yj6U3V\":[\"Нет центрального сервера:\"],\"YjvpGx\":[\"Местоимения\"],\"YqH4l4\":[\"Без ключа\"],\"YyUPpV\":[\"Аккаунт:\"],\"Z7ZXbT\":[\"Одобрить\"],\"ZJSWfw\":[\"Сообщение, отображаемое при отключении от сервера\"],\"ZR1dJ4\":[\"Приглашения\"],\"ZdWg0V\":[\"Открыть в браузере\"],\"ZhRBbl\":[\"Поиск сообщений…\"],\"Zmcu3y\":[\"Расширенные фильтры\"],\"ZqLD8l\":[\"По всему серверу\"],\"a2/8e5\":[\"Тема установлена после (мин назад)\"],\"aHKcKc\":[\"Предыдущая страница\"],\"aJTbXX\":[\"Пароль оператора\"],\"aP9gNu\":[\"вывод обрезан\"],\"aQryQv\":[\"Такой шаблон уже существует\"],\"aW9pLN\":[\"Максимальное количество пользователей в канале. Оставьте пустым для снятия ограничения.\"],\"ah4fmZ\":[\"Также показывает превью с YouTube, Vimeo, SoundCloud и других известных сервисов.\"],\"aifXak\":[\"В этом канале нет медиафайлов\"],\"ap2zBz\":[\"Мягкий\"],\"az8lvo\":[\"Выкл.\"],\"azXSNo\":[\"Развернуть список участников\"],\"azdliB\":[\"Войти в аккаунт\"],\"b26wlF\":[\"она/её\"],\"bD/+Ei\":[\"Строгий\"],\"bFDO8z\":[\"gateway в сети\"],\"bQ6BJn\":[\"Настройте подробные правила защиты от флуда. Каждое правило задаёт тип активности для мониторинга и действие при превышении порога.\"],\"bVBC/W\":[\"Gateway подключён\"],\"beV7+y\":[\"Пользователь получит приглашение вступить в \",[\"channelName\"],\".\"],\"bk84cH\":[\"Сообщение об отсутствии\"],\"bkHdLj\":[\"Добавить IRC-сервер\"],\"bmQLn5\":[\"Добавить правило\"],\"bv4cFj\":[\"Транспорт\"],\"bwRvnp\":[\"Действие\"],\"c8+EVZ\":[\"Верифицированный аккаунт\"],\"cGYUlD\":[\"Предпросмотр медиа не загружается.\"],\"cLF98o\":[\"Показать комментарии (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Нет доступных пользователей\"],\"cSgpoS\":[\"Закрепить личный чат\"],\"cde3ce\":[\"Написать <0>\",[\"0\"],\"\"],\"chQsxg\":[\"Копировать форматированный вывод\"],\"cl/A5J\":[\"Добро пожаловать на \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Удалить\"],\"coPLXT\":[\"Мы не храним ваши IRC-переписки на наших серверах\"],\"crYH/6\":[\"Плеер SoundCloud\"],\"d3sis4\":[\"Добавить сервер\"],\"d9aN5k\":[\"Удалить \",[\"username\"],\" из канала\"],\"dEgA5A\":[\"Отмена\"],\"dGi1We\":[\"Открепить эту личную переписку\"],\"dJVuyC\":[\"покинул \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"кому\"],\"dRqrdL\":[[\"0\"],\" должно быть целым числом.\"],\"dXqxlh\":[\"<0>⚠️ Угроза безопасности! Это соединение может быть уязвимо для перехвата или атак типа «человек посередине».\"],\"da9Q/R\":[\"Изменил режимы канала\"],\"dhJN3N\":[\"Показать комментарии\"],\"dj2xTE\":[\"Закрыть уведомление\"],\"dnUmOX\":[\"В этой сети пока не зарегистрировано ни одного бота.\"],\"dpCzmC\":[\"Настройки защиты от флуда\"],\"e7KzRG\":[[\"0\"],\" шаг(ов)\"],\"e9dQpT\":[\"Открыть эту ссылку в новой вкладке?\"],\"ePK91l\":[\"Изменить\"],\"eYBDuB\":[\"Загрузите изображение или укажите URL с необязательной подстановкой \",[\"size\"],\" для динамического масштабирования\"],\"edBbee\":[\"Забанить \",[\"username\"],\" по hostmask (запрещает переподключение с того же IP/хоста)\"],\"ekfzWq\":[\"Настройки пользователя\"],\"elPDWs\":[\"Настройте IRC-клиент под себя\"],\"eu2osY\":[\"<0>💡 Рекомендация: Продолжайте только если вы доверяете этому серверу и понимаете риски. Избегайте передачи конфиденциальной информации или паролей через это соединение.\"],\"euEhbr\":[\"Нажмите, чтобы войти в \",[\"channel\"]],\"ez3vLd\":[\"Включить многострочный ввод\"],\"f0J5Ki\":[\"Межсерверное взаимодействие может использовать незашифрованные соединения\"],\"f9BHJk\":[\"Предупредить пользователя\"],\"fDOLLd\":[\"Каналы не найдены.\"],\"fYdEvu\":[\"История рабочих процессов (\",[\"0\"],\")\"],\"ffzDkB\":[\"Анонимная аналитика:\"],\"fq1GF9\":[\"Показывать, когда пользователи отключаются от сервера\"],\"gEF57C\":[\"Этот сервер поддерживает только один тип подключения\"],\"gJuLUI\":[\"Список игнорирования\"],\"gNzMrk\":[\"Текущий аватар\"],\"gjPWyO\":[\"Введите ник...\"],\"gz6UQ3\":[\"Развернуть\"],\"h6razj\":[\"Исключить маску имени канала\"],\"hG6jnw\":[\"Тема не задана\"],\"hG89Ed\":[\"Изображение\"],\"hYgDIe\":[\"Создать\"],\"hZ6znB\":[\"Порт\"],\"ha+Bz5\":[\"например: 100:1440\"],\"hctjqj\":[\"Выберите бота слева, чтобы увидеть его команды и действия управления.\"],\"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\":[\"Загрузить аватар\"],\"jUXib7\":[\"Сообщение с ответом больше не отображается\"],\"jW5Uwh\":[\"Управление загрузкой внешних медиафайлов. Выкл / Безопасно / Доверенные источники / Весь контент.\"],\"jXzms5\":[\"Параметры вложения\"],\"jZlrte\":[\"Цвет\"],\"jfC/xh\":[\"Контакт\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Загрузить старые сообщения\"],\"k3ID0F\":[\"Фильтр участников…\"],\"k65gsE\":[\"Подробнее\"],\"k7Zgob\":[\"Отменить подключение\"],\"kAVx5h\":[\"Приглашения не найдены\"],\"kCLEPU\":[\"Подключён к\"],\"kF5LKb\":[\"Игнорируемые шаблоны:\"],\"kG2fiE\":[\"задан в конфигурации\"],\"kGeOx/\":[\"Присоединиться к \",[\"0\"]],\"kITKr8\":[\"Загрузка режимов канала...\"],\"kPpPsw\":[\"Вы являетесь IRC-оператором\"],\"kWJmRL\":[\"ты\"],\"kfcRb0\":[\"Аватар\"],\"kjMqSj\":[\"Копировать JSON\"],\"krViRy\":[\"Нажмите для копирования как JSON\"],\"ks71ra\":[\"Исключения\"],\"kw4lRv\":[\"Полуоператор канала\"],\"kxgIRq\":[\"Выберите или добавьте канал для начала.\"],\"ky2mw7\":[\"через @\",[\"0\"]],\"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\":[\"Обновить\"],\"lH+ed1\":[\"Ожидание первого шага…\"],\"lHy8N5\":[\"Загрузка дополнительных каналов...\"],\"lasgrr\":[\"использовано\"],\"lbpf14\":[\"Войти в \",[\"value\"]],\"lf3MT4\":[\"Канал, который покинуть (по умолчанию — текущий)\"],\"lfFsZ4\":[\"Каналы\"],\"lkNdiH\":[\"Имя аккаунта\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Загрузить изображение\"],\"loQxaJ\":[\"Я вернулся\"],\"lvfaxv\":[\"ГЛАВНАЯ\"],\"m16xKo\":[\"Добавить\"],\"m8flAk\":[\"Предпросмотр (ещё не загружено)\"],\"mDkV0w\":[\"Запуск рабочего процесса…\"],\"mEPxTp\":[\"<0>⚠️ Будьте осторожны! Открывайте ссылки только из доверенных источников. Вредоносные ссылки могут угрожать вашей безопасности или конфиденциальности.\"],\"mHGdhG\":[\"Информация о сервере\"],\"mHS8lb\":[\"Сообщение #\",[\"0\"]],\"mHfd/S\":[\"Что вы делаете\"],\"mMYBD9\":[\"Широкий — более широкая область защиты\"],\"mTGsPd\":[\"Тема канала\"],\"mU8j6O\":[\"Без внешних сообщений (+n)\"],\"mZp8FL\":[\"Автоматический возврат к однострочному режиму\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"Комментарии (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Пользователь аутентифицирован\"],\"mwtcGl\":[\"Закрыть комментарии\"],\"mzI/c+\":[\"Скачать\"],\"n3fGRk\":[\"установил \",[\"0\"]],\"nE9jsU\":[\"Мягкий — менее строгая защита\"],\"nNflMD\":[\"Покинуть канал\"],\"nPXkBi\":[\"Загрузка данных WHOIS...\"],\"nQnxxF\":[\"Сообщение #\",[\"0\"],\" (Shift+Enter — новая строка)\"],\"nWMRxa\":[\"Открепить\"],\"nX4XLG\":[\"Действия оператора\"],\"nkC032\":[\"Без профиля флуда\"],\"o69z4d\":[\"Отправить предупреждение пользователю \",[\"username\"]],\"o9ylQi\":[\"Найдите GIF для начала\"],\"oFGkER\":[\"Уведомления сервера\"],\"oOi11l\":[\"Прокрутить вниз\"],\"oPYIL5\":[\"сеть\"],\"oQEzQR\":[\"Новое DM\"],\"oXOSPE\":[\"В сети\"],\"oaTtrx\":[\"Поиск ботов\"],\"oal760\":[\"Возможны атаки типа «человек посередине» на межсерверные соединения\"],\"oeqmmJ\":[\"Доверенные источники\"],\"optX0N\":[[\"0\"],\" ч. назад\"],\"ovBPCi\":[\"По умолчанию\"],\"p0Z69r\":[\"Шаблон не может быть пустым\"],\"p1KgtK\":[\"Не удалось загрузить аудио\"],\"p59pEv\":[\"Подробности\"],\"p7sRI6\":[\"Сообщать другим, когда вы печатаете\"],\"pBm1od\":[\"Секретный канал\"],\"pNmiXx\":[\"Ваш никнейм по умолчанию для всех серверов\"],\"pQBYsE\":[\"Ответил в чате\"],\"pUUo9G\":[\"Хост:\"],\"pVGPmz\":[\"Пароль аккаунта\"],\"peNE68\":[\"Навсегда\"],\"plhHQt\":[\"Нет данных\"],\"pm6+q5\":[\"Предупреждение безопасности\"],\"pn5qSs\":[\"Дополнительная информация\"],\"q0cR4S\":[\"теперь известен как **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Канал не будет отображаться в командах LIST и NAMES\"],\"qLpTm/\":[\"Убрать реакцию \",[\"emoji\"]],\"qVkGWK\":[\"Закрепить\"],\"qXgujk\":[\"Отправить действие / эмоцию\"],\"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\"],\" имеет следующие проблемы безопасности:\"],\"sGH11W\":[\"Сервер\"],\"sHI1H+\":[\"теперь известен как **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" пригласил вас присоединиться к \",[\"channel\"]],\"sW5OjU\":[\"обязательно\"],\"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\":[\"Сохранить\"],\"thC9Rq\":[\"Покинуть канал\"],\"tiBsJk\":[\"покинул \",[\"channelName\"]],\"tt4/UD\":[\"вышел (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Канал для входа (#имя)\"],\"u0TcnO\":[\"Ник {nick} уже используется, повторная попытка с {newNick}\"],\"u0a8B4\":[\"Аутентифицироваться как IRC-оператор для административного доступа\"],\"u0rWFU\":[\"Создан после (мин назад)\"],\"u72w3t\":[\"Пользователи и шаблоны для игнорирования\"],\"u7jc2L\":[\"вышел\"],\"uAQUqI\":[\"Статус\"],\"uB85T3\":[\"Ошибка сохранения: \",[\"msg\"]],\"uMIUx8\":[\"Удалить бота \",[\"0\"],\"? Это мягко удаляет строку в базе данных; повторно использовать ник можно только после /REHASH.\"],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-серверы:\"],\"ukyW4o\":[\"Ваши пригласительные ссылки\"],\"usSSr/\":[\"Масштаб\"],\"v7uvcf\":[\"Программа:\"],\"vE8kb+\":[\"Shift+Enter для новой строки (Enter отправляет)\"],\"vERlcd\":[\"Профиль\"],\"vK0RL8\":[\"Без темы\"],\"vSJd18\":[\"Видео\"],\"vXIe7J\":[\"Язык\"],\"vaHYxN\":[\"Настоящее имя\"],\"vhjbKr\":[\"Отсутствую\"],\"w4NYox\":[\"клиент \",[\"title\"]],\"w8xQRx\":[\"Неверное значение\"],\"wCKe3+\":[\"История рабочих процессов\"],\"wFjjxZ\":[\"был кикнут из \",[\"channelName\"],\" пользователем \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Исключения из банов не найдены\"],\"wPrGnM\":[\"Администратор канала\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Рассуждение\"],\"wbm86v\":[\"Показывать, когда пользователи входят в каналы или покидают их\"],\"wdxz7K\":[\"Источник\"],\"whqZ9r\":[\"Дополнительные слова или фразы для выделения\"],\"wm7RV4\":[\"Звук уведомления\"],\"wz/Yoq\":[\"Ваши сообщения могут быть перехвачены при передаче между серверами\"],\"x3+y8b\":[\"Столько человек зарегистрировалось по этой ссылке\"],\"xCJdfg\":[\"Очистить\"],\"xOTzt5\":[\"только что\"],\"xUHRTR\":[\"Автоматически аутентифицироваться как оператор при подключении\"],\"xWHwwQ\":[\"Баны\"],\"xYilR2\":[\"Медиа\"],\"xbi8D6\":[\"Этот сервер не поддерживает пригласительные ссылки (возможность<0>obby.world/invitationне объявлена). Вы по-прежнему можете общаться в чате как обычно; эта панель предназначена для сетей на базе obbyircd.\"],\"xceQrO\":[\"Поддерживаются только защищённые WebSocket-соединения\"],\"xdtXa+\":[\"имя-канала\"],\"xeiujy\":[\"Текст\"],\"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\"]],\"zPBDzU\":[\"Отменить рабочий процесс\"],\"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..a40dfe9e 100644 --- a/src/locales/ru/messages.po +++ b/src/locales/ru/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Перенося IRC в будущее" msgid "— open in viewer" msgstr "— открыть в просмотрщике" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(обрабатывается ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Показать ещё 1 элемент} few {Пока msgid "{0} and {1} are typing..." msgstr "{0} и {1} печатают..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} обязателен." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} печатает..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} должно быть числом." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} должно быть целым числом." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} старых сообщений" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} шаг(ов)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} шаг(ов) ожидает подтверждения" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Расширенные фильтры" msgid "Album" msgstr "Альбом" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Все" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Весь контент" @@ -297,6 +334,12 @@ msgstr "Применить фильтры и обновить" msgid "Applying..." msgstr "Применяется..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Одобрить" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Аутентифицированный аккаунт" msgid "Auto Fallback to Single Line" msgstr "Автоматический возврат к однострочному режиму" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Автоматическое закрытие через {secondsLeft}с" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Автоматически аутентифицироваться как оператор при подключении" @@ -359,6 +406,10 @@ msgstr "Отсутствую" msgid "Away from keyboard" msgstr "Отошёл от клавиатуры" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Сообщение об отсутствии" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Сообщение об отсутствии" msgid "Away:" msgstr "Отсутствие:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Баны" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Бот" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Бот ещё не зарегистрировал ни одной слэш-команды." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Боты" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Боты в этой сети" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Просмотреть все каналы на сервере" @@ -430,6 +499,7 @@ msgstr "Просмотреть все каналы на сервере" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Отменить подключение" msgid "Cancel reply" msgstr "Отменить ответ" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Отменить рабочий процесс" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Изменить имя канала (только для операторов)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Изменить ваш никнейм на этом сервере" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Изменил режимы канала" @@ -459,6 +537,7 @@ msgstr "Изменил никнейм" msgid "changed the topic to: {topic}" msgstr "изменил тему на: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Канал" @@ -519,6 +598,14 @@ msgstr "Владелец канала" msgid "Channel Settings" msgstr "Настройки канала" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Канал для входа (#имя)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Канал, который покинуть (по умолчанию — текущий)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Тема канала" @@ -531,6 +618,7 @@ msgstr "Канал не будет отображаться в командах msgid "channel-name" msgstr "имя-канала" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Каналы" @@ -606,6 +694,9 @@ msgstr "Лимит пользователей (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Комментарии" msgid "Comments ({commentCount})" msgstr "Комментарии ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "задан в конфигурации" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Бот, заданный в конфигурации. Отредактируйте obbyircd.conf и выполните /REHASH, чтобы изменить состояние." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "Настройте подробные правила защиты от флуда. Каждое правило задаёт тип активности для мониторинга и действие при превышении порога." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Никнейм по умолчанию" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Удалить" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Удалить бота {0}? Это мягко удаляет строку в базе данных; повторно использовать ник можно только после /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Удалить канал" @@ -843,6 +948,10 @@ msgstr "Обзор" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Откройте мир IRC вместе с ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Закрыть" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Закрыть уведомление" @@ -1039,6 +1148,10 @@ msgstr "Фильтр каналов..." msgid "Filter members…" msgstr "Фильтр участников…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Первое сообщение для отправки" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Профиль флуда (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (глобальный бан)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway подключён" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway в сети" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Основное" @@ -1186,6 +1307,10 @@ msgstr "Изображение {0} из {1}" msgid "Image preview" msgstr "Предпросмотр изображения" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "ВХОД" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Инфо:" @@ -1270,6 +1395,10 @@ msgstr "Присоединиться к {0}" msgid "Join {value}" msgstr "Войти в {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Войти в канал" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Подробнее о пользовательских правилах msgid "Learn more about profiles →" msgstr "Подробнее о профилях →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Покинуть канал" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Покинуть канал" @@ -1411,6 +1544,14 @@ msgstr "Управление информацией профиля и метад msgid "Mark as bot - usually 'on' or empty" msgstr "Пометить как бота — обычно «on» или пусто" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Отметить себя как отошедшего" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Отметить себя как вернувшегося" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Макс. пользователей" @@ -1573,6 +1714,10 @@ msgstr "Название сети" msgid "New DM" msgstr "Новое DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Новый никнейм" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Следующее изображение" @@ -1617,6 +1762,10 @@ msgstr "Исключения из банов не найдены" msgid "No bans found" msgstr "Баны не найдены" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "В этой сети пока не зарегистрировано ни одного бота." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Нет центрального сервера:" @@ -1672,7 +1821,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" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC — IRC будущего" msgid "Off" msgstr "Выкл." +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "не в сети" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Не в сети" @@ -1754,6 +1908,10 @@ msgstr "Не в сети" msgid "on" msgstr "вкл" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "один из:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Упс! Разрыв сети! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Открыть приватное сообщение пользователю" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Открыть настройки конфигурации канала" @@ -1828,10 +1990,23 @@ msgstr "Пароль оператора" msgid "Oper Username" msgstr "Имя пользователя оператора" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Действия оператора" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Необязательные отчёты о сбоях для улучшения приложения" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "ВЫХОД" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "вывод обрезан" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Владелец" @@ -1994,6 +2169,10 @@ msgstr "Сообщение при выходе" msgid "Quit the server" msgstr "Покинул сервер" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "выполнил" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Реакция" @@ -2024,6 +2203,10 @@ msgstr "Причина" msgid "Reason (optional)" msgstr "Причина (необязательно)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Рассуждение" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Переподключиться к серверу" @@ -2037,6 +2220,11 @@ msgstr "Обновить" msgid "Register for an account" msgstr "Зарегистрировать аккаунт" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Отклонить" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Мягкий" @@ -2077,11 +2265,27 @@ msgstr "Переименовать этот канал на сервере. Вс msgid "Render markdown formatting in messages" msgstr "Отображать форматирование Markdown в сообщениях" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Снова открыть рабочий процесс, создавший это сообщение ({stepCount} шагов)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Ответить" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "обязательно" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Ответил в чате" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Сообщение с ответом больше не отображается" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Повторить отправку" msgid "Rules" msgstr "Правила" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Выполнить" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Безопасно" @@ -2121,6 +2329,10 @@ msgstr "Сохранено" msgid "Saving..." msgstr "Сохранение..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Прокрутить чат к этому ответу" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Прокрутить вниз" msgid "Search" msgstr "Поиск" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Поиск ботов" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Найдите GIF для начала" @@ -2183,6 +2399,10 @@ msgstr "Предупреждение безопасности" msgid "Seek" msgstr "Перемотка" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Выберите бота слева, чтобы увидеть его команды и действия управления." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Выберите канал" @@ -2195,6 +2415,10 @@ msgstr "Выбрать участника" msgid "Select or add a channel to get started." msgstr "Выберите или добавьте канал для начала." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "самостоятельно зарегистрирован" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Отправить GIF" msgid "Send a warning message to {username}" msgstr "Отправить предупреждение пользователю {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Отправить действие / эмоцию" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Отправить приглашение" @@ -2280,6 +2508,10 @@ msgstr "Пароль сервера" msgid "Server-to-server communication may use unencrypted connections" msgstr "Межсерверное взаимодействие может использовать незашифрованные соединения" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "По всему серверу" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Задать тему…" @@ -2376,6 +2608,10 @@ msgstr "Показывает медиафайлы с доверенного фа msgid "Signed On" msgstr "В сети с" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Слэш-команды" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Программа:" @@ -2392,10 +2628,18 @@ msgstr "Сортировать по пользователям" msgid "SoundCloud player" msgstr "Плеер SoundCloud" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Источник" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Начать личный чат" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Запуск рабочего процесса…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Статусные сообщения" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Стоп" @@ -2419,10 +2664,18 @@ msgstr "Строгий" msgid "Strict - More aggressive protection" msgstr "Строгий — более агрессивная защита" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Приостановить" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Система" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Текст" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Текстовые каналы" @@ -2447,6 +2700,14 @@ msgstr "Тема, которая будет отображаться для эт msgid "The user will receive an invitation to join {channelName}." msgstr "Пользователь получит приглашение вступить в {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Рабочий процесс, создавший это сообщение, больше не находится в состоянии" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Эта команда не принимает параметров." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Это поле обязательно для заполнения" @@ -2510,6 +2771,10 @@ msgstr "Включить/выключить уведомления" msgid "Toggle search" msgstr "Включить/выключить поиск" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Инструмент" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Тема установлена после (мин назад)" @@ -2527,6 +2792,10 @@ msgstr "Тема:" msgid "Total: {0}" msgstr "Всего: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Транспорт" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Доверенные источники" @@ -2561,6 +2830,10 @@ msgstr "Открепить личный чат" msgid "Unpin this private message conversation" msgstr "Открепить эту личную переписку" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Возобновить" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Загрузить" @@ -2687,6 +2960,11 @@ msgstr "Очень мягкий" msgid "Very Strict" msgstr "Очень строгий" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "через @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Видео" @@ -2730,6 +3008,10 @@ msgstr "Пользователь с голосом" msgid "Volume" msgstr "Громкость" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Ожидание первого шага…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Был исключён из канала" msgid "We don't store your IRC communications on our servers" msgstr "Мы не храним ваши IRC-переписки на наших серверах" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Добро пожаловать на {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Чем ты занимаешься?" msgid "What this means:" msgstr "Что это означает:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Что вы делаете" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "О чём вы думаете?" @@ -2780,6 +3070,10 @@ msgstr "О чём вы думаете?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Шепнуть пользователю в контексте текущего канала" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Широкий — более широкая область защиты" @@ -2788,6 +3082,19 @@ msgstr "Широкий — более широкая область защиты msgid "Will default to 'no reason' if left empty" msgstr "Если оставить пустым, будет использоваться «без причины»" +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "История рабочих процессов" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "История рабочих процессов ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Выполняется…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/sv/messages.mjs b/src/locales/sv/messages.mjs index f51da7e2..84cc5528 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: 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\"],\"\"],\"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! 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: 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! Ö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\"],\" 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/invitationannonseras 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\"],\"+fRR7i\":[\"Stäng av\"],\"+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\"],\")\"],\"/0mUpR\":[\"Markera dig själv som tillbaka\"],\"/3BQ4J\":[\"Användare utanför kanalen kan inte skicka meddelanden till den\"],\"/4C8U0\":[\"Kopiera allt\"],\"/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\"],\"/xQ19T\":[\"Bottar i detta nätverk\"],\"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\"],\"1C/fOn\":[\"Botten har inte registrerat några snedstreckskommandon än.\"],\"1DSr1i\":[\"Registrera ett konto\"],\"1O/24y\":[\"Växla kanallista\"],\"1QfxQT\":[\"Avfärda\"],\"1VPJJ2\":[\"Varning för extern länk\"],\"1ZC/dv\":[\"Inga olästa omnämnanden eller meddelanden\"],\"1pO1zi\":[\"Servernamn krävs\"],\"1t/NnN\":[\"Avvisa\"],\"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\"],\"2xXP/g\":[\"Gå med i en kanal\"],\"2ygf/L\":[\"← Tillbaka\"],\"2zEgxj\":[\"Sök GIF:ar...\"],\"3JjdaA\":[\"Kör\"],\"3NJ4MW\":[\"Öppna arbetsflödet som skapade detta meddelande igen (\",[\"stepCount\"],\" steg)\"],\"3RdPhl\":[\"Byt namn på kanal\"],\"3THokf\":[\"Voice-användare\"],\"3TSz9S\":[\"Minimera\"],\"3et0TM\":[\"Bläddra chatten till detta svar\"],\"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\"],\"4uKgKr\":[\"UT\"],\"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:\"],\"9q17ZR\":[[\"0\"],\" krävs.\"],\"9qIYMn\":[\"Nytt smeknamn\"],\"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?\"],\"BIDT9R\":[\"Bottar\"],\"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\"],\"CaQ1Gb\":[\"Konfigurationsdefinierad bot. Redigera obbyircd.conf och kör /REHASH för att ändra tillstånd.\"],\"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\"],\"DSHF2K\":[\"Arbetsflödet som skapade detta meddelande finns inte längre i tillståndet\"],\"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...\"],\"Eu7YKa\":[\"självregistrerad\"],\"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: 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...\"],\"IcHxhR\":[\"offline\"],\"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\"],\"JMXMCX\":[\"Bortameddelande\"],\"JXGkhG\":[\"Ändra kanalnamnet (endast operatörer)\"],\"JYiL1b\":[\"en av:\"],\"JcD7qf\":[\"Fler åtgärder\"],\"JdkA+c\":[\"Hemlig (+s)\"],\"Jmu12l\":[\"Serverkanaler\"],\"JvQ++s\":[\"Aktivera Markdown\"],\"K2jwh/\":[\"Ingen WHOIS-data tillgänglig\"],\"K4vEhk\":[\"(avstängd)\"],\"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\"],\"KtADxr\":[\"körde\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" steg väntar på godkännande\"],\"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\\\")\"],\"LYzbQ2\":[\"Verktyg\"],\"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\"],\"M45wtf\":[\"Detta kommando tar inga parametrar.\"],\"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\"],\"N40H+G\":[\"Alla\"],\"N7TQbE\":[\"Bjud in användare till \",[\"channelName\"]],\"NCca/o\":[\"Ange standardsmeknamn...\"],\"NQN2HS\":[\"Återaktivera\"],\"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+HhhG\":[\"Viska till en användare i den aktuella kanalkontexten\"],\"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\"]],\"RlCInP\":[\"Snedstreckskommandon\"],\"RySp6q\":[\"Dölj kommentarer\"],\"RzfkXn\":[\"Byt ditt smeknamn på denna server\"],\"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\"],\"TImSWn\":[\"(hanteras av ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Markera dig själv som borta\"],\"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!\"],\"UqtiKk\":[\"Stängs automatiskt om \",[\"secondsLeft\"],\" s\"],\"UrEy4W\":[\"Öppna ett privat meddelande till en användare\"],\"UtUIRh\":[[\"0\"],\" äldre meddelanden\"],\"UwzP+U\":[\"Säker anslutning\"],\"V0/A4O\":[\"Kanalägare\"],\"V2dwib\":[[\"0\"],\" måste vara ett tal.\"],\"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\"],\"XklovM\":[\"Arbetar…\"],\"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+tK3n\":[\"Första meddelandet att skicka\"],\"Y/qryO\":[\"Inga användare hittades som matchar din sökning\"],\"YAqRpI\":[\"Kontoregistrering för \",[\"account\"],\" lyckades: \",[\"message\"]],\"YBXJ7j\":[\"IN\"],\"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:\"],\"Z7ZXbT\":[\"Godkänn\"],\"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\"],\"ZqLD8l\":[\"Hela servern\"],\"a2/8e5\":[\"Ämne angivet efter (min sedan)\"],\"aHKcKc\":[\"Föregående sida\"],\"aJTbXX\":[\"Oper-lösenord\"],\"aP9gNu\":[\"utdata avkortad\"],\"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\"],\"bFDO8z\":[\"gateway online\"],\"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.\"],\"bVBC/W\":[\"Gateway ansluten\"],\"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\"],\"cde3ce\":[\"Meddelande <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\"],\"dRqrdL\":[[\"0\"],\" måste vara ett heltal.\"],\"dXqxlh\":[\"<0>⚠️ Säkerhetsrisk! 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\"],\"dnUmOX\":[\"Inga bottar registrerade i detta nätverk än.\"],\"dpCzmC\":[\"Inställningar för flödeskydd\"],\"e7KzRG\":[[\"0\"],\" steg\"],\"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: 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.\"],\"fYdEvu\":[\"Arbetsflödeshistorik (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Välj en bot till vänster för att se dess kommandon och hanteringsåtgärder.\"],\"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\"],\"jUXib7\":[\"Svarsmeddelandet visas inte längre\"],\"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:\"],\"kG2fiE\":[\"konfigurationsdefinierad\"],\"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.\"],\"ky2mw7\":[\"via @\",[\"0\"]],\"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\"],\"lH+ed1\":[\"Väntar på första steget…\"],\"lHy8N5\":[\"Laddar fler kanaler...\"],\"lasgrr\":[\"använd\"],\"lbpf14\":[\"Gå med i \",[\"value\"]],\"lf3MT4\":[\"Kanal att lämna (standard är aktuell)\"],\"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)\"],\"mDkV0w\":[\"Startar arbetsflöde…\"],\"mEPxTp\":[\"<0>⚠️ Var försiktig! Öppna bara länkar från betrodda källor. Skadliga länkar kan äventyra din säkerhet eller integritet.\"],\"mHGdhG\":[\"Serverinformation\"],\"mHS8lb\":[\"Meddelande #\",[\"0\"]],\"mHfd/S\":[\"Vad du gör\"],\"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\"],\"nX4XLG\":[\"Operatörsåtgärder\"],\"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\"],\"oaTtrx\":[\"Sök bottar\"],\"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\"],\"pQBYsE\":[\"Svarade i chatten\"],\"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\"],\"qXgujk\":[\"Skicka en handling / emote\"],\"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\"],\" 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\"]],\"sW5OjU\":[\"obligatoriskt\"],\"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\"],\"thC9Rq\":[\"Lämna en kanal\"],\"tiBsJk\":[\"lämnade \",[\"channelName\"]],\"tt4/UD\":[\"lämnade (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Kanal att gå med i (#namn)\"],\"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\"]],\"uMIUx8\":[\"Ta bort botten \",[\"0\"],\"? Detta mjukraderar databasraden; återanvänd smeknamnet senare först efter en /REHASH.\"],\"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\"],\"wCKe3+\":[\"Arbetsflödeshistorik\"],\"wFjjxZ\":[\"kastades ut från \",[\"channelName\"],\" av \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Inga banundantag hittades\"],\"wPrGnM\":[\"Kanaladmin\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Resonemang\"],\"wbm86v\":[\"Visa när användare går med i eller lämnar kanaler\"],\"wdxz7K\":[\"Källa\"],\"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/invitationannonseras 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\"],\"xeiujy\":[\"Text\"],\"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\"]],\"zPBDzU\":[\"Avbryt arbetsflöde\"],\"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..51d12eb4 100644 --- a/src/locales/sv/messages.po +++ b/src/locales/sv/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - För IRC in i framtiden" msgid "— open in viewer" msgstr "— öppna i visaren" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(hanteras av ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(avstängd)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Visa 1 till} other {Visa {1} till}}" msgid "{0} and {1} are typing..." msgstr "{0} och {1} skriver..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} krävs." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} skriver..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} måste vara ett tal." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} måste vara ett heltal." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} äldre meddelanden" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} steg" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} steg väntar på godkännande" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Avancerade filter" msgid "Album" msgstr "Album" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Alla" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Allt innehåll" @@ -297,6 +334,12 @@ msgstr "Tillämpa filter och uppdatera" msgid "Applying..." msgstr "Tillämpar..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Godkänn" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Autentiserat konto" msgid "Auto Fallback to Single Line" msgstr "Automatisk återgång till enkel rad" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Stängs automatiskt om {secondsLeft} s" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Autentisera automatiskt som operatör vid anslutning" @@ -359,6 +406,10 @@ msgstr "Borta" msgid "Away from keyboard" msgstr "Borta från tangentbordet" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Bortameddelande" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Borta-meddelande" msgid "Away:" msgstr "Frånvarande:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Banningar" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Botten har inte registrerat några snedstreckskommandon än." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Bottar" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Bottar i detta nätverk" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Bläddra bland alla kanaler på servern" @@ -430,6 +499,7 @@ msgstr "Bläddra bland alla kanaler på servern" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Avbryt anslutning" msgid "Cancel reply" msgstr "Avbryt svar" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Avbryt arbetsflöde" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Ändra kanalnamnet (endast operatörer)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Byt ditt smeknamn på denna server" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Ändrade kanallägen" @@ -459,6 +537,7 @@ msgstr "Bytte nick" msgid "changed the topic to: {topic}" msgstr "ändrade ämnet till: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Kanal" @@ -519,6 +598,14 @@ msgstr "Kanalägare" msgid "Channel Settings" msgstr "Kanalinställningar" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Kanal att gå med i (#namn)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Kanal att lämna (standard är aktuell)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Kanalämne" @@ -531,6 +618,7 @@ msgstr "Kanalen visas inte i LIST- eller NAMES-kommandon" msgid "channel-name" msgstr "kanalnamn" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Kanaler" @@ -606,6 +694,9 @@ msgstr "Klientgräns (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Kommentarer" msgid "Comments ({commentCount})" msgstr "Kommentarer ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "konfigurationsdefinierad" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Konfigurationsdefinierad bot. Redigera obbyircd.conf och kör /REHASH för att ändra tillstånd." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "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." @@ -715,7 +814,7 @@ msgstr "Kopiera" #: src/components/ui/RawLogViewer.tsx msgid "Copy all" -msgstr "Kopiera alla" +msgstr "Kopiera allt" #: src/components/message/JsonLogMessage.tsx msgid "Copy entire JSON" @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Standard-nick" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Ta bort" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Ta bort botten {0}? Detta mjukraderar databasraden; återanvänd smeknamnet senare först efter en /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Ta bort kanal" @@ -843,6 +948,10 @@ msgstr "Utforska" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Utforska IRC-världen med ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Avfärda" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Stäng avisering" @@ -1039,6 +1148,10 @@ msgstr "Filtrera kanaler..." msgid "Filter members…" msgstr "Filtrera medlemmar…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Första meddelandet att skicka" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Flödesprofil (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (globalt ban)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway ansluten" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway online" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Allmänt" @@ -1186,6 +1307,10 @@ msgstr "Bild {0} av {1}" msgid "Image preview" msgstr "Bildförhandsvisning" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "IN" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Info:" @@ -1270,6 +1395,10 @@ msgstr "Gå med i {0}" msgid "Join {value}" msgstr "Gå med i {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Gå med i en kanal" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Läs mer om anpassade regler →" msgid "Learn more about profiles →" msgstr "Läs mer om profiler →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Lämna en kanal" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Lämna kanal" @@ -1411,6 +1544,14 @@ msgstr "Hantera din profilinformation och metadata" msgid "Mark as bot - usually 'on' or empty" msgstr "Markera som bot – vanligtvis 'on' eller tomt" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Markera dig själv som borta" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Markera dig själv som tillbaka" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Max användare" @@ -1573,6 +1714,10 @@ msgstr "Nätverksnamn" msgid "New DM" msgstr "Nytt DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Nytt smeknamn" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Nästa bild" @@ -1617,6 +1762,10 @@ msgstr "Inga banundantag hittades" msgid "No bans found" msgstr "Inga banningar hittades" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Inga bottar registrerade i detta nätverk än." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Ingen central server:" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - För IRC in i framtiden" msgid "Off" msgstr "Av" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "offline" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Offline" @@ -1754,6 +1908,10 @@ msgstr "Offline" msgid "on" msgstr "på" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "en av:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Hoppsan! Nätverksuppdelning! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Öppna ett privat meddelande till en användare" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Öppna kanalens konfigurationsinställningar" @@ -1828,10 +1990,23 @@ msgstr "Oper-lösenord" msgid "Oper Username" msgstr "Oper-användarnamn" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Operatörsåtgärder" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Valfria kraschrapporter för att förbättra appen" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "UT" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "utdata avkortad" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Ägare" @@ -1994,6 +2169,10 @@ msgstr "Quit-meddelande" msgid "Quit the server" msgstr "Lämnade servern" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "körde" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Reagera" @@ -2024,6 +2203,10 @@ msgstr "Anledning" msgid "Reason (optional)" msgstr "Anledning (valfritt)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Resonemang" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Återanslut till server" @@ -2037,6 +2220,11 @@ msgstr "Uppdatera" msgid "Register for an account" msgstr "Registrera ett konto" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Avvisa" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Avslappnat" @@ -2077,11 +2265,27 @@ msgstr "Byt namn på den här kanalen på servern. Alla användare ser det nya n msgid "Render markdown formatting in messages" msgstr "Rendera markdown-formatering i meddelanden" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Öppna arbetsflödet som skapade detta meddelande igen ({stepCount} steg)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Svara" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "obligatoriskt" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Svarade i chatten" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Svarsmeddelandet visas inte längre" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Försök skicka igen" msgid "Rules" msgstr "Regler" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Kör" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Säkert" @@ -2121,6 +2329,10 @@ msgstr "Sparat" msgid "Saving..." msgstr "Sparar..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Bläddra chatten till detta svar" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Scrolla till botten" msgid "Search" msgstr "Sök" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Sök bottar" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Sök efter GIF:ar för att komma igång" @@ -2183,6 +2399,10 @@ msgstr "Säkerhetsvarning" msgid "Seek" msgstr "Sök position" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Välj en bot till vänster för att se dess kommandon och hanteringsåtgärder." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Välj en kanal" @@ -2195,6 +2415,10 @@ msgstr "Välj medlem" msgid "Select or add a channel to get started." msgstr "Välj eller lägg till en kanal för att komma igång." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "självregistrerad" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Skicka en GIF" msgid "Send a warning message to {username}" msgstr "Skicka ett varningsmeddelande till {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Skicka en handling / emote" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Skicka inbjudan" @@ -2280,6 +2508,10 @@ msgstr "Serverlösenord" msgid "Server-to-server communication may use unencrypted connections" msgstr "Server-till-server-kommunikation kan använda okrypterade anslutningar" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "Hela servern" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Ange ett ämne…" @@ -2376,6 +2608,10 @@ msgstr "Visar media från serverns betrodda filvärd. Inga begäranden görs til msgid "Signed On" msgstr "Inloggad sedan" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Snedstreckskommandon" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Programvara:" @@ -2392,10 +2628,18 @@ msgstr "Sortera efter användare" msgid "SoundCloud player" msgstr "SoundCloud-spelare" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Källa" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Starta privat meddelande" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Startar arbetsflöde…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Statusmeddelanden" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Stoppa" @@ -2419,10 +2664,18 @@ msgstr "Strikt" msgid "Strict - More aggressive protection" msgstr "Strikt – mer aggressivt skydd" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Stäng av" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "System" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Text" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Textkanaler" @@ -2447,6 +2700,14 @@ msgstr "Ämnet som visas för den här kanalen. Alla användare kan se ämnet." msgid "The user will receive an invitation to join {channelName}." msgstr "Användaren får en inbjudan att gå med i {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Arbetsflödet som skapade detta meddelande finns inte längre i tillståndet" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Detta kommando tar inga parametrar." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Det här fältet krävs" @@ -2510,6 +2771,10 @@ msgstr "Växla aviseringar" msgid "Toggle search" msgstr "Växla sökning" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Verktyg" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Ämne angivet efter (min sedan)" @@ -2527,6 +2792,10 @@ msgstr "Ämne:" msgid "Total: {0}" msgstr "Totalt: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Transport" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Betrodda källor" @@ -2561,6 +2830,10 @@ msgstr "Lossa privatchatt" msgid "Unpin this private message conversation" msgstr "Lossa den här privata meddelandekonversationen" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Återaktivera" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Ladda upp" @@ -2687,6 +2960,11 @@ msgstr "Mycket avslappnat" msgid "Very Strict" msgstr "Mycket strikt" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "via @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2730,6 +3008,10 @@ msgstr "Voice-användare" msgid "Volume" msgstr "Volym" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Väntar på första steget…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Sparkades ut från kanalen" msgid "We don't store your IRC communications on our servers" msgstr "Vi lagrar inte dina IRC-kommunikationer på våra servrar" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Välkommen till {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Vad håller du på med?" msgid "What this means:" msgstr "Vad detta innebär:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Vad du gör" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "Vad tänker du på?" @@ -2780,6 +3070,10 @@ msgstr "Vad tänker du på?" msgid "WHISPER" msgstr "VISKNING" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Viska till en användare i den aktuella kanalkontexten" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Brett – bredare skyddsomfång" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Arbetsflödeshistorik" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Arbetsflödeshistorik ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Arbetar…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/tr/messages.mjs b/src/locales/tr/messages.mjs index 99836297..183c558e 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: 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\"],\"'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! 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: 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! 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\"],\" 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/invitation 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\"],\"+fRR7i\":[\"Askıya al\"],\"+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\"],\")\"],\"/0mUpR\":[\"Kendinizi geri dönmüş olarak işaretleyin\"],\"/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\"],\"/xQ19T\":[\"Bu ağdaki botlar\"],\"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\"],\"1C/fOn\":[\"Bot henüz hiçbir eğik çizgi komutu kaydetmedi.\"],\"1DSr1i\":[\"Hesap kaydı oluştur\"],\"1O/24y\":[\"Kanal Listesini Aç/Kapat\"],\"1QfxQT\":[\"Kapat\"],\"1VPJJ2\":[\"Harici Bağlantı Uyarısı\"],\"1ZC/dv\":[\"Okunmamış bahis veya mesaj yok\"],\"1pO1zi\":[\"Sunucu adı gereklidir\"],\"1t/NnN\":[\"Reddet\"],\"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\"],\"2xXP/g\":[\"Bir kanala katıl\"],\"2ygf/L\":[\"← Geri\"],\"2zEgxj\":[\"GIF ara...\"],\"3JjdaA\":[\"Çalıştır\"],\"3NJ4MW\":[\"Bu mesajı oluşturan iş akışını yeniden aç (\",[\"stepCount\"],\" adım)\"],\"3RdPhl\":[\"Kanalı Yeniden Adlandır\"],\"3THokf\":[\"Sesli Kullanıcı\"],\"3TSz9S\":[\"Küçült\"],\"3et0TM\":[\"Sohbeti bu yanıta kaydır\"],\"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ç\"],\"4uKgKr\":[\"GİDEN\"],\"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ı buraya 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:\"],\"9q17ZR\":[[\"0\"],\" gereklidir.\"],\"9qIYMn\":[\"Yeni takma ad\"],\"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?\"],\"BIDT9R\":[\"Botlar\"],\"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\"],\"CaQ1Gb\":[\"Yapılandırmada tanımlı bot. Durumu değiştirmek için obbyircd.conf dosyasını düzenleyin ve /REHASH yapın.\"],\"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ı\"],\"DSHF2K\":[\"Bu mesajı oluşturan iş akışı artık durumda değil\"],\"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...\"],\"Eu7YKa\":[\"kendi kaydı\"],\"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: 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...\"],\"IcHxhR\":[\"çevrimdışı\"],\"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\"],\"JMXMCX\":[\"Uzakta mesajı\"],\"JXGkhG\":[\"Kanal adını değiştir (yalnızca operatörler)\"],\"JYiL1b\":[\"şunlardan biri:\"],\"JcD7qf\":[\"Daha fazla işlem\"],\"JdkA+c\":[\"Gizli (+s)\"],\"Jmu12l\":[\"Sunucu Kanalları\"],\"JvQ++s\":[\"Markdown'ı Etkinleştir\"],\"K2jwh/\":[\"WHOIS verisi mevcut değil\"],\"K4vEhk\":[\"(askıya alındı)\"],\"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\"],\"KtADxr\":[\"çalıştırdı\"],\"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.\"],\"LN3RO2\":[[\"0\"],\" adım onay bekliyor\"],\"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\\\")\"],\"LYzbQ2\":[\"Araç\"],\"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\"],\"M45wtf\":[\"Bu komut parametre almıyor.\"],\"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ş\"],\"N40H+G\":[\"Tümü\"],\"N7TQbE\":[[\"channelName\"],\" kanalına Kullanıcı Davet Et\"],\"NCca/o\":[\"Varsayılan takma adı girin...\"],\"NQN2HS\":[\"Askıdan al\"],\"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+HhhG\":[\"Geçerli kanal bağlamında bir kullanıcıya fısılda\"],\"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\"]],\"RlCInP\":[\"Eğik çizgi komutları\"],\"RySp6q\":[\"Yorumları gizle\"],\"RzfkXn\":[\"Bu sunucudaki takma adınızı değiştirin\"],\"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\"],\"TImSWn\":[\"(ObsidianIRC tarafından işlenir)\"],\"TRDppN\":[\"Webhook\"],\"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\"],\"U7yg75\":[\"Kendinizi uzakta olarak işaretleyin\"],\"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!\"],\"UqtiKk\":[[\"secondsLeft\"],\" sn içinde otomatik kapanır\"],\"UrEy4W\":[\"Bir kullanıcıya özel mesaj aç\"],\"UtUIRh\":[[\"0\"],\" eski mesaj\"],\"UwzP+U\":[\"Güvenli Bağlantı\"],\"V0/A4O\":[\"Kanal Sahibi\"],\"V2dwib\":[[\"0\"],\" bir sayı olmalıdır.\"],\"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\"],\"XklovM\":[\"Çalışıyor…\"],\"Xm/s+u\":[\"Görünüm\"],\"Xp2n93\":[\"Sunucunuzun güvenilir dosya hostundan medya gösterir. Harici hizmetlere istek gönderilmez.\"],\"XvjC4F\":[\"Kaydediliyor...\"],\"Y+tK3n\":[\"Gönderilecek ilk mesaj\"],\"Y/qryO\":[\"Aramanızla eşleşen kullanıcı bulunamadı\"],\"YAqRpI\":[[\"account\"],\" hesap kaydı başarılı: \",[\"message\"]],\"YBXJ7j\":[\"GELEN\"],\"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:\"],\"Z7ZXbT\":[\"Onayla\"],\"ZJSWfw\":[\"Sunucudan ayrıldığınızda gösterilen mesaj\"],\"ZR1dJ4\":[\"Davetler\"],\"ZdWg0V\":[\"Tarayıcıda aç\"],\"ZhRBbl\":[\"Mesajlarda ara…\"],\"Zmcu3y\":[\"Gelişmiş Filtreler\"],\"ZqLD8l\":[\"Sunucu genelinde\"],\"a2/8e5\":[\"Şu kadar dakika önce sonra konu belirlenen\"],\"aHKcKc\":[\"Önceki sayfa\"],\"aJTbXX\":[\"Oper Şifresi\"],\"aP9gNu\":[\"çıktı kısaltıldı\"],\"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ı\"],\"bFDO8z\":[\"gateway çevrimiçi\"],\"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.\"],\"bVBC/W\":[\"Gateway bağlandı\"],\"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\"],\"cde3ce\":[\"<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\"],\"dRqrdL\":[[\"0\"],\" bir tam sayı olmalıdır.\"],\"dXqxlh\":[\"<0>⚠️ Güvenlik Riski! 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\"],\"dnUmOX\":[\"Bu ağda henüz kayıtlı bot yok.\"],\"dpCzmC\":[\"Flood Koruma Ayarları\"],\"e7KzRG\":[[\"0\"],\" adım\"],\"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: 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ı.\"],\"fYdEvu\":[\"İş akışı geçmişi (\",[\"0\"],\")\"],\"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\"],\"hctjqj\":[\"Komutlarını ve yönetim işlemlerini görmek için soldan bir bot seçin.\"],\"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\"],\"jUXib7\":[\"Yanıt mesajı artık görünümde değil\"],\"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:\"],\"kG2fiE\":[\"yapılandırmada tanımlı\"],\"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.\"],\"ky2mw7\":[\"@\",[\"0\"],\" üzerinden\"],\"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\"],\"lH+ed1\":[\"İlk adım bekleniyor…\"],\"lHy8N5\":[\"Daha fazla kanal yükleniyor...\"],\"lasgrr\":[\"kullanıldı\"],\"lbpf14\":[[\"value\"],\" kanalına katıl\"],\"lf3MT4\":[\"Ayrılınacak kanal (varsayılan: geçerli)\"],\"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)\"],\"mDkV0w\":[\"İş akışı başlatılıyor…\"],\"mEPxTp\":[\"<0>⚠️ Dikkatli olun! 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\"],\"mHfd/S\":[\"Ne yapıyorsunuz\"],\"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\"],\"nX4XLG\":[\"Operator işlemleri\"],\"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\"],\"oaTtrx\":[\"Botları ara\"],\"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\"],\"pQBYsE\":[\"Sohbette yanıtladı\"],\"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\"],\"qXgujk\":[\"Bir eylem / emote gönder\"],\"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\"],\" 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\"],\"sW5OjU\":[\"gerekli\"],\"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\"],\"thC9Rq\":[\"Bir kanaldan ayrıl\"],\"tiBsJk\":[[\"channelName\"],\" kanalından ayrıldı\"],\"tt4/UD\":[\"ayrıldı (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Katılınacak kanal (#ad)\"],\"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\"]],\"uMIUx8\":[[\"0\"],\" botu silinsin mi? Bu, veritabanı satırını yumuşak siler; takma adı yalnızca bir /REHASH sonrasında yeniden kullanın.\"],\"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\"],\"wCKe3+\":[\"İş akışı geçmişi\"],\"wFjjxZ\":[[\"channelName\"],\" kanalından \",[\"username\"],\" tarafından atıldı (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Yasak istisnası bulunamadı\"],\"wPrGnM\":[\"Kanal Yöneticisi\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Akıl yürütme\"],\"wbm86v\":[\"Kullanıcılar kanala katıldığında veya ayrıldığında göster\"],\"wdxz7K\":[\"Kaynak\"],\"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/invitation 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ı\"],\"xeiujy\":[\"Metin\"],\"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\"]],\"zPBDzU\":[\"İş akışını iptal et\"],\"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..eb137cfc 100644 --- a/src/locales/tr/messages.po +++ b/src/locales/tr/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - IRC'yi geleceğe taşıyor" msgid "— open in viewer" msgstr "— görüntüleyicide aç" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(ObsidianIRC tarafından işlenir)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +msgstr "(askıya alındı)" + #. 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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {1 öğe daha göster} other {{1} öğe daha göster}}" msgid "{0} and {1} are typing..." msgstr "{0} ve {1} yazıyor..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} gereklidir." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} yazıyor..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} bir sayı olmalıdır." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} bir tam sayı olmalıdır." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} eski mesaj" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} adım" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} adım onay bekliyor" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Gelişmiş Filtreler" msgid "Album" msgstr "Albüm" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Tümü" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Tüm İçerik" @@ -297,6 +334,12 @@ msgstr "Filtreleri Uygula ve Yenile" msgid "Applying..." msgstr "Uygulanıyor..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Onayla" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Doğrulanmış Hesap" msgid "Auto Fallback to Single Line" msgstr "Otomatik Tek Satıra Dön" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "{secondsLeft} sn içinde otomatik kapanır" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Bağlanırken otomatik olarak operatör kimliği doğrula" @@ -359,6 +406,10 @@ msgstr "Uzakta" msgid "Away from keyboard" msgstr "Klavyeden uzakta" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Uzakta mesajı" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Uzakta Mesajı" msgid "Away:" msgstr "Uzakta:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Yasaklar" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Bot" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Bot henüz hiçbir eğik çizgi komutu kaydetmedi." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Botlar" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Bu ağdaki botlar" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Sunucudaki tüm kanalları görüntüle" @@ -430,6 +499,7 @@ msgstr "Sunucudaki tüm kanalları görüntüle" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Bağlantıyı İptal Et" msgid "Cancel reply" msgstr "Yanıtı iptal et" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "İş akışını iptal et" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Kanal adını değiştir (yalnızca operatörler)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Bu sunucudaki takma adınızı değiştirin" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Kanal modları değiştirildi" @@ -459,6 +537,7 @@ msgstr "Takma ad değiştirildi" msgid "changed the topic to: {topic}" msgstr "konuyu şu şekilde değiştirdi: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Kanal" @@ -519,6 +598,14 @@ msgstr "Kanal Sahibi" msgid "Channel Settings" msgstr "Kanal Ayarları" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Katılınacak kanal (#ad)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Ayrılınacak kanal (varsayılan: geçerli)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Kanal Konusu" @@ -531,6 +618,7 @@ msgstr "Kanal LIST veya NAMES komutlarında görünmeyecek" msgid "channel-name" msgstr "kanal-adı" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Kanallar" @@ -606,6 +694,9 @@ msgstr "İstemci Sınırı (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Yorumlar" msgid "Comments ({commentCount})" msgstr "Yorumlar ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "yapılandırmada tanımlı" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Yapılandırmada tanımlı bot. Durumu değiştirmek için obbyircd.conf dosyasını düzenleyin ve /REHASH yapın." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "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." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Varsayılan Takma Ad" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Sil" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "{0} botu silinsin mi? Bu, veritabanı satırını yumuşak siler; takma adı yalnızca bir /REHASH sonrasında yeniden kullanın." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Kanalı Sil" @@ -843,6 +948,10 @@ msgstr "Keşfet" msgid "Discover the world of IRC with ObsidianIRC" msgstr "ObsidianIRC ile IRC dünyasını keşfedin" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Kapat" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Bildirimi kapat" @@ -891,7 +1000,7 @@ 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ı buraya bırakın" #: src/components/ui/ChannelSettingsModal.tsx msgid "e.g., 100:1440" @@ -1039,6 +1148,10 @@ msgstr "Kanalları filtrele..." msgid "Filter members…" msgstr "Üyeleri filtrele…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Gönderilecek ilk mesaj" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Flood Profili (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (global yasak)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway bağlandı" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway çevrimiçi" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Genel" @@ -1186,6 +1307,10 @@ msgstr "{1} görselinden {0}. görsel" msgid "Image preview" msgstr "Görüntü önizlemesi" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "GELEN" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Bilgi:" @@ -1270,6 +1395,10 @@ msgstr "{0} kanalına katıl" msgid "Join {value}" msgstr "{value} kanalına katıl" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Bir kanala katıl" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Özel kurallar hakkında daha fazla bilgi →" msgid "Learn more about profiles →" msgstr "Profiller hakkında daha fazla bilgi →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Bir kanaldan ayrıl" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Kanaldan ayrıl" @@ -1411,6 +1544,14 @@ msgstr "Profil bilgilerinizi ve meta verilerinizi yönetin" msgid "Mark as bot - usually 'on' or empty" msgstr "Bot olarak işaretle - genellikle 'on' veya boş" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Kendinizi uzakta olarak işaretleyin" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Kendinizi geri dönmüş olarak işaretleyin" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Maks. Kullanıcı" @@ -1573,6 +1714,10 @@ msgstr "Ağ Adı" msgid "New DM" msgstr "Yeni DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Yeni takma ad" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Sonraki görüntü" @@ -1617,6 +1762,10 @@ msgstr "Yasak istisnası bulunamadı" msgid "No bans found" msgstr "Yasak bulunamadı" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "Bu ağda henüz kayıtlı bot yok." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Merkezi Sunucu Yok:" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - IRC'yi geleceğe taşıyor" msgid "Off" msgstr "Kapalı" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "çevrimdışı" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Çevrimdışı" @@ -1754,6 +1908,10 @@ msgstr "Çevrimdışı" msgid "on" msgstr "açık" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "şunlardan biri:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Eyvah! Ağ bölündü! ⚠️" msgid "Op" msgstr "Op" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Bir kullanıcıya özel mesaj aç" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Kanal yapılandırma ayarlarını aç" @@ -1828,10 +1990,23 @@ msgstr "Oper Şifresi" msgid "Oper Username" msgstr "Oper Kullanıcı Adı" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Operator işlemleri" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Uygulamayı geliştirmek için isteğe bağlı çökme raporları" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "GİDEN" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "çıktı kısaltıldı" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Sahip" @@ -1994,6 +2169,10 @@ msgstr "Ayrılma Mesajı" msgid "Quit the server" msgstr "Sunucudan ayrıldı" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "çalıştırdı" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "Tepki Ver" @@ -2024,6 +2203,10 @@ msgstr "Neden" msgid "Reason (optional)" msgstr "Neden (isteğe bağlı)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Akıl yürütme" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Sunucuya yeniden bağlan" @@ -2037,6 +2220,11 @@ msgstr "Yenile" msgid "Register for an account" msgstr "Hesap kaydı oluştur" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Reddet" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Rahat" @@ -2077,11 +2265,27 @@ msgstr "Bu kanalı sunucuda yeniden adlandır. Tüm kullanıcılar yeni adı gö msgid "Render markdown formatting in messages" msgstr "Mesajlarda markdown biçimlendirmesini işle" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Bu mesajı oluşturan iş akışını yeniden aç ({stepCount} adım)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Yanıtla" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "gerekli" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Sohbette yanıtladı" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Yanıt mesajı artık görünümde değil" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Yeniden gönder" msgid "Rules" msgstr "Kurallar" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Çalıştır" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Güvenli" @@ -2121,6 +2329,10 @@ msgstr "Kaydedildi" msgid "Saving..." msgstr "Kaydediliyor..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Sohbeti bu yanıta kaydır" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "En alta kaydır" msgid "Search" msgstr "Ara" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Botları ara" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Başlamak için GIF arayın" @@ -2183,6 +2399,10 @@ msgstr "Güvenlik Uyarısı" msgid "Seek" msgstr "Konuma git" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Komutlarını ve yönetim işlemlerini görmek için soldan bir bot seçin." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Bir kanal seçin" @@ -2195,6 +2415,10 @@ msgstr "Üye Seç" msgid "Select or add a channel to get started." msgstr "Başlamak için bir kanal seçin veya ekleyin." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "kendi kaydı" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "GIF gönder" msgid "Send a warning message to {username}" msgstr "{username} kullanıcısına uyarı mesajı gönder" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Bir eylem / emote gönder" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Davet Gönder" @@ -2280,6 +2508,10 @@ msgstr "Sunucu Şifresi" msgid "Server-to-server communication may use unencrypted connections" msgstr "Sunucular arası iletişim şifrelenmemiş bağlantılar kullanıyor olabilir" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "Sunucu genelinde" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Konu belirle…" @@ -2376,6 +2608,10 @@ msgstr "Sunucunuzun güvenilir dosya hostundan medya gösterir. Harici hizmetler msgid "Signed On" msgstr "Giriş Yapıldı" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Eğik çizgi komutları" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Yazılım:" @@ -2392,10 +2628,18 @@ msgstr "Kullanıcıya Göre Sırala" msgid "SoundCloud player" msgstr "SoundCloud oynatıcı" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Kaynak" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Özel Mesaj Başlat" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "İş akışı başlatılıyor…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Durum Mesajları" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Durdur" @@ -2419,10 +2664,18 @@ msgstr "Katı" msgid "Strict - More aggressive protection" msgstr "Katı - Daha agresif koruma" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Askıya al" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Sistem" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Metin" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Metin Kanalları" @@ -2447,6 +2700,14 @@ msgstr "Bu kanal için görüntülenecek konu. Tüm kullanıcılar konuyu göreb msgid "The user will receive an invitation to join {channelName}." msgstr "Kullanıcı {channelName} kanalına katılmak için davet alacak." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Bu mesajı oluşturan iş akışı artık durumda değil" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Bu komut parametre almıyor." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Bu alan zorunludur" @@ -2510,6 +2771,10 @@ msgstr "Bildirimleri Aç/Kapat" msgid "Toggle search" msgstr "Aramayı aç/kapat" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Araç" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Şu kadar dakika önce sonra konu belirlenen" @@ -2527,6 +2792,10 @@ msgstr "Konu:" msgid "Total: {0}" msgstr "Toplam: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Taşıma" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Güvenilir Kaynaklar" @@ -2561,6 +2830,10 @@ msgstr "Özel Sohbetin Sabitlemesini Kaldır" msgid "Unpin this private message conversation" msgstr "Bu özel mesaj konuşmasının sabitlemesini kaldır" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Askıdan al" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Yükle" @@ -2687,6 +2960,11 @@ msgstr "Çok Rahat" msgid "Very Strict" msgstr "Çok Katı" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "@{0} üzerinden" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Video" @@ -2730,6 +3008,10 @@ msgstr "Sesli Kullanıcı" msgid "Volume" msgstr "Ses Seviyesi" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "İlk adım bekleniyor…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Kanaldan atıldı" msgid "We don't store your IRC communications on our servers" msgstr "IRC iletişimlerinizi sunucularımızda saklamıyoruz" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "{__DEFAULT_IRC_SERVER_NAME__}'a hoş geldiniz!" @@ -2772,6 +3058,10 @@ msgstr "Ne yapıyorsun?" msgid "What this means:" msgstr "Bu ne anlama geliyor:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Ne yapıyorsunuz" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "Aklınızda ne var?" @@ -2780,6 +3070,10 @@ msgstr "Aklınızda ne var?" msgid "WHISPER" msgstr "FISISALTI" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Geçerli kanal bağlamında bir kullanıcıya fısılda" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Geniş - Daha kapsamlı koruma alanı" @@ -2788,6 +3082,19 @@ 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/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "İş akışı geçmişi" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "İş akışı geçmişi ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Çalışıyor…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/uk/messages.mjs b/src/locales/uk/messages.mjs index 6bc57b7d..6b3ef631 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>Ризик: Конфіденційна інформація (повідомлення, приватні розмови, дані автентифікації) може бути доступна мережевим адміністраторам або зловмисникам між 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\"],\"\"],\"chQsxg\":[\"Скопіювати форматований вивід\"],\"cl/A5J\":[\"Ласкаво просимо до \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Видалити\"],\"coPLXT\":[\"Ми не зберігаємо ваші IRC-комунікації на наших серверах\"],\"crYH/6\":[\"Програвач SoundCloud\"],\"d3sis4\":[\"Додати сервер\"],\"d9aN5k\":[\"Видалити \",[\"username\"],\" з каналу\"],\"dEgA5A\":[\"Скасувати\"],\"dGi1We\":[\"Відкріпити цю приватну розмову\"],\"dJVuyC\":[\"покинув \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"до\"],\"dXqxlh\":[\"<0>⚠️ Загроза безпеці! Це з'єднання може бути вразливим до перехоплення або атак «людина посередині».\"],\"da9Q/R\":[\"Змінено режими каналу\"],\"dhJN3N\":[\"Показати коментарі\"],\"dj2xTE\":[\"Відхилити сповіщення\"],\"dpCzmC\":[\"Налаштування захисту від флуду\"],\"e9dQpT\":[\"Бажаєте відкрити це посилання в новій вкладці?\"],\"ePK91l\":[\"Редагувати\"],\"eYBDuB\":[\"Завантажте зображення або вкажіть URL з необов'язковою підстановкою \",[\"size\"],\" для динамічного розміру\"],\"edBbee\":[\"Заблокувати \",[\"username\"],\" за маскою хоста (запобігає повторному входу з тієї ж IP/хоста)\"],\"ekfzWq\":[\"Налаштування користувача\"],\"elPDWs\":[\"Налаштуйте свій IRC-клієнт\"],\"eu2osY\":[\"<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>⚠️ Будьте обережні! Відкривайте лише посилання з надійних джерел. Шкідливі посилання можуть порушити вашу безпеку або конфіденційність.\"],\"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\"],\" має такі проблеми безпеки:\"],\"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/invitationне оголошена). Ви все ще можете нормально спілкуватися; ця панель призначена для мереж на основі 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\":[\"Стандартне повідомлення при позначенні себе відсутнім\"],\"+fRR7i\":[\"Призупинити\"],\"+mVPqU\":[\"Відображати Markdown-форматування у повідомленнях\"],\"+vqCJH\":[\"Ім'я користувача вашого акаунту для автентифікації\"],\"+yPBXI\":[\"Вибрати файл\"],\"+zy2Nq\":[\"Тип\"],\"/09cao\":[\"Низький рівень безпеки з'єднання (Рівень \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"Позначити себе як повернувся\"],\"/3BQ4J\":[\"Користувачі за межами каналу не можуть надсилати до нього повідомлення\"],\"/4C8U0\":[\"Копіювати все\"],\"/6BzZF\":[\"Перемкнути список учасників\"],\"/AkXyp\":[\"Підтвердити?\"],\"/TNOPk\":[\"Користувач відсутній\"],\"/XQgft\":[\"Дослідити\"],\"/cF7Rs\":[\"Гучність\"],\"/dqduX\":[\"Наступна сторінка\"],\"/fc3q4\":[\"Весь вміст\"],\"/kISDh\":[\"Увімкнути звуки сповіщень\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Аудіо\"],\"/rfkZe\":[\"Відтворювати звуки для згадок і повідомлень\"],\"/xQ19T\":[\"Боти в цій мережі\"],\"0/0ZGA\":[\"Маска назви каналу\"],\"0D6j7U\":[\"Дізнатися більше про власні правила →\"],\"0XsHcR\":[\"Вигнати користувача\"],\"0ZpE//\":[\"Сортувати за кількістю користувачів\"],\"0bEPwz\":[\"Встановити статус «Відсутній»\"],\"0dGkPt\":[\"Розгорнути список каналів\"],\"0gS7M5\":[\"Відображуване ім'я\"],\"0kS+M8\":[\"ПрикладМЕРЕЖА\"],\"0rgoY7\":[\"Підключатися лише до обраних вами серверів\"],\"0wdd7X\":[\"Приєднатися\"],\"0wkVYx\":[\"Приватні повідомлення\"],\"111uHX\":[\"Попередній перегляд посилання\"],\"196EG4\":[\"Видалити приватний чат\"],\"1C/fOn\":[\"Бот ще не зареєстрував жодної слеш-команди.\"],\"1DSr1i\":[\"Зареєструвати акаунт\"],\"1O/24y\":[\"Перемкнути список каналів\"],\"1QfxQT\":[\"Закрити\"],\"1VPJJ2\":[\"Попередження про зовнішнє посилання\"],\"1ZC/dv\":[\"Немає непрочитаних згадувань або повідомлень\"],\"1pO1zi\":[\"Назва сервера обов'язкова\"],\"1t/NnN\":[\"Відхилити\"],\"1uwfzQ\":[\"Переглянути тему каналу\"],\"268g7c\":[\"Введіть відображуване ім'я\"],\"2F9+AZ\":[\"Сирий IRC-трафік ще не зафіксовано. Спробуйте підключитися або надіслати повідомлення.\"],\"2FOFq1\":[\"Оператори сервера в мережі можуть потенційно читати ваші повідомлення\"],\"2FYpfJ\":[\"Більше\"],\"2HF1Y2\":[[\"inviter\"],\" запросив \",[\"target\"],\" приєднатися до \",[\"channel\"]],\"2I70QL\":[\"Переглянути інформацію профілю користувача\"],\"2QYdmE\":[\"Користувачі:\"],\"2QpEjG\":[\"вийшов\"],\"2YE223\":[\"Повідомлення #\",[\"0\"],\" (Enter для нового рядка, Shift+Enter для відправки)\"],\"2bimFY\":[\"Використовувати пароль сервера\"],\"2iTmdZ\":[\"Локальне сховище:\"],\"2odkwe\":[\"Строгий - більш агресивний захист\"],\"2uDhbA\":[\"Введіть ім'я користувача для запрошення\"],\"2xXP/g\":[\"Приєднатися до каналу\"],\"2ygf/L\":[\"← Назад\"],\"2zEgxj\":[\"Пошук GIF...\"],\"3JjdaA\":[\"Виконати\"],\"3NJ4MW\":[\"Знову відкрити робочий процес, який створив це повідомлення (\",[\"stepCount\"],\" кроків)\"],\"3RdPhl\":[\"Перейменувати канал\"],\"3THokf\":[\"Користувач з голосом\"],\"3TSz9S\":[\"Згорнути\"],\"3et0TM\":[\"Прокрутити чат до цієї відповіді\"],\"3jBDvM\":[\"Відображувана назва каналу\"],\"3ryuFU\":[\"Необов'язкові звіти про збої для покращення додатку\"],\"3uBF/8\":[\"Закрити переглядач\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Введіть ім'я акаунту...\"],\"4/Rr0R\":[\"Запросити користувача до поточного каналу\"],\"4EZrJN\":[\"Правила\"],\"4JJtW9\":[\"#переповнення\"],\"4NqeT4\":[\"Профіль флуду (+F)\"],\"4RZQRK\":[\"Що ти зараз робиш?\"],\"4hfTrB\":[\"Псевдонім\"],\"4n99LO\":[\"Вже в \",[\"0\"]],\"4t6vMV\":[\"Автоматично перемикатися на один рядок для коротких повідомлень\"],\"4uKgKr\":[\"ВИХІД\"],\"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\":[\"Питання про конфіденційність? Зв'яжіться з нами:\"],\"9q17ZR\":[[\"0\"],\" обов'язковий.\"],\"9qIYMn\":[\"Новий нікнейм\"],\"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\":[\"У вас є незбережені зміни. Ви впевнені, що хочете закрити без збереження?\"],\"BIDT9R\":[\"Боти\"],\"BIf9fi\":[\"Ваше статусне повідомлення\"],\"BPm98R\":[\"Жоден сервер не вибрано. Спочатку оберіть сервер з бічної панелі; запрошувальні посилання керуються окремо для кожного сервера.\"],\"BZz3md\":[\"Ваш особистий веб-сайт\"],\"Bgm/H7\":[\"Дозволити введення кількох рядків тексту\"],\"BiQIl1\":[\"Закріпити цю приватну розмову\"],\"BlNZZ2\":[\"Натисніть, щоб перейти до повідомлення\"],\"Bowq3c\":[\"Тільки оператори можуть змінювати тему каналу\"],\"Btozzp\":[\"Термін дії цього зображення минув\"],\"Bycfjm\":[\"Всього: \",[\"0\"]],\"C6IBQc\":[\"Скопіювати весь JSON\"],\"C9L9wL\":[\"Збір даних\"],\"CDq4wC\":[\"Помірний режим для користувача\"],\"CHVRxG\":[\"Повідомлення @\",[\"0\"],\" (Shift+Enter для нового рядка)\"],\"CN9zdR\":[\"Ім'я та пароль оператора обов'язкові\"],\"CW3sYa\":[\"Додати реакцію \",[\"emoji\"]],\"CaAkqd\":[\"Показати виходи\"],\"CaQ1Gb\":[\"Бот, визначений у конфігурації. Відредагуйте obbyircd.conf і виконайте /REHASH, щоб змінити стан.\"],\"CbvaYj\":[\"Блокування за псевдонімом\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Вибрати канал\"],\"CsekCi\":[\"Нормальний\"],\"D+NlUC\":[\"Система\"],\"D28t6+\":[\"приєднався та вийшов\"],\"DB8zMK\":[\"Застосувати\"],\"DBcWHr\":[\"Власний файл звуку сповіщення\"],\"DSHF2K\":[\"Робочий процес, який створив це повідомлення, більше не перебуває у стані\"],\"DTy9Xw\":[\"Попередній перегляд медіа\"],\"Dj4pSr\":[\"Виберіть надійний пароль\"],\"Du+zn+\":[\"Пошук...\"],\"Du2T2f\":[\"Налаштування не знайдено\"],\"DwsSVQ\":[\"Застосувати фільтри та оновити\"],\"E3W/zd\":[\"Стандартний псевдонім\"],\"E6nRW7\":[\"Копіювати URL\"],\"E703RG\":[\"Режими:\"],\"EAeu1Z\":[\"Надіслати запрошення\"],\"EFKJQT\":[\"Налаштування\"],\"EGPQBv\":[\"Власні правила флуду (+f)\"],\"ELik0r\":[\"Переглянути повну політику конфіденційності\"],\"EPbeC2\":[\"Переглянути або редагувати тему каналу\"],\"EQCDNT\":[\"Введіть ім'я користувача оператора...\"],\"EUvulZ\":[\"Знайдено 1 повідомлення, що відповідає \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Наступне зображення\"],\"EdQY6l\":[\"Немає\"],\"EnqLYU\":[\"Пошук серверів...\"],\"Eu7YKa\":[\"самостійно зареєстрований\"],\"F0OKMc\":[\"Редагувати сервер\"],\"F6Int2\":[\"Увімкнути виділення\"],\"FDoLyE\":[\"Макс. користувачів\"],\"FUU/hZ\":[\"Контролюйте, скільки зовнішніх медіа завантажується у чаті.\"],\"Fdp03t\":[\"увімк\"],\"FfPWR0\":[\"Модальне вікно\"],\"FjkaiT\":[\"Зменшити\"],\"FlqOE9\":[\"Що це означає:\"],\"FolHNl\":[\"Керуйте своїм обліковим записом та автентифікацією\"],\"Fp2Dif\":[\"Вийти з сервера\"],\"G5KmCc\":[\"GZ-Line (глобальна Z-Line)\"],\"GDs0lz\":[\"<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\"],\" пишуть...\"],\"IcHxhR\":[\"не в мережі\"],\"IgrLD/\":[\"Пауза\"],\"Im6JED\":[\"ШЕПІТ\"],\"ImOQa9\":[\"Відповісти\"],\"IoHMnl\":[\"Максимальне значення: \",[\"0\"]],\"IvMj+0\":[\"Оп\"],\"J28zul\":[\"Підключення...\"],\"J5T9NW\":[\"Інформація про користувача\"],\"J8Y5+z\":[\"Ой! Розрив мережі! ⚠️\"],\"JBHkBA\":[\"Покинув канал\"],\"JCwL0Q\":[\"Введіть причину (необов'язково)\"],\"JFciKP\":[\"Перемкнути\"],\"JMXMCX\":[\"Повідомлення про відсутність\"],\"JXGkhG\":[\"Змінити назву каналу (лише оператори)\"],\"JYiL1b\":[\"один з:\"],\"JcD7qf\":[\"Більше дій\"],\"JdkA+c\":[\"Секретний (+s)\"],\"Jmu12l\":[\"Канали сервера\"],\"JvQ++s\":[\"Увімкнути Markdown\"],\"K2jwh/\":[\"Дані WHOIS недоступні\"],\"K4vEhk\":[\"(призупинено)\"],\"KAXSwC\":[\"Голос\"],\"KDfTdX\":[\"Видалити повідомлення\"],\"KKBlUU\":[\"Вбудувати\"],\"KM0pLb\":[\"Ласкаво просимо до каналу!\"],\"KR6W2h\":[\"Скасувати ігнорування\"],\"KV+Bi1\":[\"Тільки за запрошенням (+i)\"],\"KdCtwE\":[\"Скільки секунд відстежувати активність флуду перед скиданням лічильників\"],\"Kkezga\":[\"Пароль сервера\"],\"KsiQ/8\":[\"Користувачі повинні бути запрошені для входу до каналу\"],\"KtADxr\":[\"виконав\"],\"L+gB/D\":[\"Інформація про канал\"],\"LC1a7n\":[\"IRC-сервер повідомив, що зв'язки між серверами мають низький рівень безпеки. Це означає, що коли ваші повідомлення передаються між IRC-серверами в мережі, вони можуть бути не належним чином зашифровані або SSL/TLS-сертифікати можуть не перевірятися правильно.\"],\"LN3RO2\":[[\"0\"],\" крок(ів) очікує на затвердження\"],\"LNfLR5\":[\"Показати виключення\"],\"LQb0W/\":[\"Показати всі події\"],\"LU7/yA\":[\"Альтернативна назва для відображення в інтерфейсі. Може містити пробіли, емодзі та спеціальні символи. Справжня назва каналу (\",[\"channelName\"],\") використовуватиметься для IRC-команд.\"],\"LUb9O7\":[\"Потрібен дійсний порт сервера\"],\"LV4fT6\":[\"Опис (необов'язково, напр. \\\"Бета-тестувальники Q3\\\")\"],\"LYzbQ2\":[\"Інструмент\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Політика конфіденційності\"],\"LcuSDR\":[\"Керуйте інформацією профілю та метаданими\"],\"LqLS9B\":[\"Показати зміни псевдоніма\"],\"LsDQt2\":[\"Налаштування каналу\"],\"LtI9AS\":[\"Власник\"],\"LuNhhL\":[\"відреагував на це повідомлення\"],\"M/AZNG\":[\"URL зображення вашого аватара\"],\"M/WIer\":[\"Надіслати повідомлення\"],\"M45wtf\":[\"Ця команда не приймає параметрів.\"],\"M8er/5\":[\"Ім'я:\"],\"MHk+7g\":[\"Попереднє зображення\"],\"MRorGe\":[\"ПП користувачу\"],\"MVbSGP\":[\"Часове вікно (секунди)\"],\"MkpcsT\":[\"Ваші повідомлення та налаштування зберігаються локально на вашому пристрої\"],\"N/hDSy\":[\"Позначити як бот - зазвичай 'on' або порожньо\"],\"N40H+G\":[\"Усі\"],\"N7TQbE\":[\"Запросити користувача до \",[\"channelName\"]],\"NCca/o\":[\"Введіть стандартний нік...\"],\"NQN2HS\":[\"Відновити\"],\"Nqs6B9\":[\"Показує всі зовнішні медіа. Будь-яка URL може спричинити запит до невідомого сервера.\"],\"Nt+9O7\":[\"Використовувати WebSocket замість звичайного TCP\"],\"NxIHzc\":[\"Відключити користувача\"],\"O+HhhG\":[\"Шепнути користувачу в контексті поточного каналу\"],\"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\"]],\"RlCInP\":[\"Слеш-команди\"],\"RySp6q\":[\"Приховати коментарі\"],\"RzfkXn\":[\"Змінити свій нікнейм на цьому сервері\"],\"SPKQTd\":[\"Псевдонім обов'язковий\"],\"SPVjfj\":[\"За замовчуванням буде 'без причини', якщо залишити порожнім\"],\"SQKPvQ\":[\"Запросити користувача\"],\"SkZcl+\":[\"Виберіть готовий профіль захисту від флуду. Ці профілі надають збалансовані налаштування захисту для різних випадків використання.\"],\"Slr+3C\":[\"Мін. користувачів\"],\"Spnlre\":[\"Ви запросили \",[\"target\"],\" приєднатися до \",[\"channel\"]],\"T/ckN5\":[\"Відкрити у переглядачі\"],\"T91vKp\":[\"Відтворити\"],\"TImSWn\":[\"(обробляється ObsidianIRC)\"],\"TRDppN\":[\"Webhook\"],\"TV2Wdu\":[\"Дізнайтесь, як ми обробляємо ваші дані та захищаємо вашу конфіденційність.\"],\"TgFpwD\":[\"Застосовується...\"],\"TkzSFB\":[\"Без змін\"],\"TtserG\":[\"Введіть справжнє ім'я\"],\"Ttz9J1\":[\"Введіть пароль...\"],\"Tz0i8g\":[\"Налаштування\"],\"U3pytU\":[\"Адмін\"],\"U7yg75\":[\"Позначити себе як відсутнього\"],\"UDb2YD\":[\"React\"],\"UE4KO5\":[\"*канал*\"],\"UETAwW\":[\"Ви ще не створили жодного запрошувального посилання. Скористайтеся формою вище, щоб створити перше.\"],\"UGT5vp\":[\"Зберегти налаштування\"],\"UV5hLB\":[\"Заборон не знайдено\"],\"Uaj3Nd\":[\"Статусні повідомлення\"],\"Ue3uny\":[\"За замовчуванням (без профілю)\"],\"UkARhe\":[\"Нормальний - стандартний захист\"],\"Umn7Cj\":[\"Коментарів ще немає. Будьте першим!\"],\"UqtiKk\":[\"Автоматичне закриття через \",[\"secondsLeft\"],\"с\"],\"UrEy4W\":[\"Відкрити приватне повідомлення з користувачем\"],\"UtUIRh\":[[\"0\"],\" старіших повідомлень\"],\"UwzP+U\":[\"Безпечне з'єднання\"],\"V0/A4O\":[\"Власник каналу\"],\"V2dwib\":[[\"0\"],\" має бути числом.\"],\"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\":[\"Відкріпити приватну розмову\"],\"XklovM\":[\"Виконується…\"],\"Xm/s+u\":[\"Дисплей\"],\"Xp2n93\":[\"Показує медіа з надійного файлового хоста вашого сервера. Запити до зовнішніх сервісів не здійснюються.\"],\"XvjC4F\":[\"Збереження...\"],\"Y+tK3n\":[\"Перше повідомлення для надсилання\"],\"Y/qryO\":[\"Користувачів за вашим запитом не знайдено\"],\"YAqRpI\":[\"Реєстрація облікового запису \",[\"account\"],\" успішна: \",[\"message\"]],\"YBXJ7j\":[\"ВХІД\"],\"YEfzvP\":[\"Захищена тема (+t)\"],\"YQOn6a\":[\"Згорнути список учасників\"],\"YRCoE9\":[\"Оператор каналу\"],\"YURQaF\":[\"Переглянути профіль\"],\"YdBSvr\":[\"Керувати відображенням медіа та зовнішнього вмісту\"],\"Yj6U3V\":[\"Без центрального сервера:\"],\"YjvpGx\":[\"Займенники\"],\"YqH4l4\":[\"Без ключа\"],\"YyUPpV\":[\"Акаунт:\"],\"Z7ZXbT\":[\"Затвердити\"],\"ZJSWfw\":[\"Повідомлення при від'єднанні від сервера\"],\"ZR1dJ4\":[\"Запрошення\"],\"ZdWg0V\":[\"Відкрити в браузері\"],\"ZhRBbl\":[\"Пошук повідомлень…\"],\"Zmcu3y\":[\"Розширені фільтри\"],\"ZqLD8l\":[\"По всьому серверу\"],\"a2/8e5\":[\"Тему встановлено після (хв тому)\"],\"aHKcKc\":[\"Попередня сторінка\"],\"aJTbXX\":[\"Пароль оператора\"],\"aP9gNu\":[\"вивід скорочено\"],\"aQryQv\":[\"Шаблон вже існує\"],\"aW9pLN\":[\"Максимальна кількість користувачів у каналі. Залиште порожнім для відсутності обмежень.\"],\"ah4fmZ\":[\"Також показує попередній перегляд з YouTube, Vimeo, SoundCloud та подібних відомих сервісів.\"],\"aifXak\":[\"Немає медіа у цьому каналі\"],\"ap2zBz\":[\"Розслаблений\"],\"az8lvo\":[\"Вимк\"],\"azXSNo\":[\"Розгорнути список учасників\"],\"azdliB\":[\"Увійти в акаунт\"],\"b26wlF\":[\"вона/її\"],\"bD/+Ei\":[\"Строгий\"],\"bFDO8z\":[\"gateway у мережі\"],\"bQ6BJn\":[\"Налаштуйте детальні правила захисту від флуду. Кожне правило вказує, яку активність відстежувати і які дії вживати при перевищенні порогів.\"],\"bVBC/W\":[\"Gateway підключено\"],\"beV7+y\":[\"Користувач отримає запрошення приєднатися до \",[\"channelName\"],\".\"],\"bk84cH\":[\"Повідомлення про відсутність\"],\"bkHdLj\":[\"Додати IRC-сервер\"],\"bmQLn5\":[\"Додати правило\"],\"bv4cFj\":[\"Транспорт\"],\"bwRvnp\":[\"Дія\"],\"c8+EVZ\":[\"Підтверджений акаунт\"],\"cGYUlD\":[\"Медіа-перегляди не завантажені.\"],\"cLF98o\":[\"Показати коментарі (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Користувачів немає\"],\"cSgpoS\":[\"Закріпити приватну розмову\"],\"cde3ce\":[\"Повідомлення <0>\",[\"0\"],\"\"],\"chQsxg\":[\"Скопіювати форматований вивід\"],\"cl/A5J\":[\"Ласкаво просимо до \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Видалити\"],\"coPLXT\":[\"Ми не зберігаємо ваші IRC-комунікації на наших серверах\"],\"crYH/6\":[\"Програвач SoundCloud\"],\"d3sis4\":[\"Додати сервер\"],\"d9aN5k\":[\"Видалити \",[\"username\"],\" з каналу\"],\"dEgA5A\":[\"Скасувати\"],\"dGi1We\":[\"Відкріпити цю приватну розмову\"],\"dJVuyC\":[\"покинув \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"до\"],\"dRqrdL\":[[\"0\"],\" має бути цілим числом.\"],\"dXqxlh\":[\"<0>⚠️ Загроза безпеці! Це з'єднання може бути вразливим до перехоплення або атак «людина посередині».\"],\"da9Q/R\":[\"Змінено режими каналу\"],\"dhJN3N\":[\"Показати коментарі\"],\"dj2xTE\":[\"Відхилити сповіщення\"],\"dnUmOX\":[\"У цій мережі ще не зареєстровано жодного бота.\"],\"dpCzmC\":[\"Налаштування захисту від флуду\"],\"e7KzRG\":[[\"0\"],\" крок(ів)\"],\"e9dQpT\":[\"Бажаєте відкрити це посилання в новій вкладці?\"],\"ePK91l\":[\"Редагувати\"],\"eYBDuB\":[\"Завантажте зображення або вкажіть URL з необов'язковою підстановкою \",[\"size\"],\" для динамічного розміру\"],\"edBbee\":[\"Заблокувати \",[\"username\"],\" за маскою хоста (запобігає повторному входу з тієї ж IP/хоста)\"],\"ekfzWq\":[\"Налаштування користувача\"],\"elPDWs\":[\"Налаштуйте свій IRC-клієнт\"],\"eu2osY\":[\"<0>💡 Рекомендація: Продовжуйте лише якщо довіряєте цьому серверу і розумієте ризики. Уникайте передачі конфіденційної інформації або паролів через це з'єднання.\"],\"euEhbr\":[\"Натисніть, щоб приєднатися до \",[\"channel\"]],\"ez3vLd\":[\"Увімкнути багаторядкове введення\"],\"f0J5Ki\":[\"Зв'язок між серверами може використовувати незашифровані з'єднання\"],\"f9BHJk\":[\"Попередити користувача\"],\"fDOLLd\":[\"Канали не знайдено.\"],\"fYdEvu\":[\"Історія робочих процесів (\",[\"0\"],\")\"],\"ffzDkB\":[\"Анонімна аналітика:\"],\"fq1GF9\":[\"Відображати, коли користувачі від'єднуються від сервера\"],\"gEF57C\":[\"Цей сервер підтримує лише один тип з'єднання\"],\"gJuLUI\":[\"Список ігнорування\"],\"gNzMrk\":[\"Поточний аватар\"],\"gjPWyO\":[\"Введіть нік...\"],\"gz6UQ3\":[\"Розгорнути\"],\"h6razj\":[\"Маска виключення назви каналу\"],\"hG6jnw\":[\"Тема не встановлена\"],\"hG89Ed\":[\"Зображення\"],\"hYgDIe\":[\"Створити\"],\"hZ6znB\":[\"Порт\"],\"ha+Bz5\":[\"напр., 100:1440\"],\"hctjqj\":[\"Виберіть бота ліворуч, щоб побачити його команди та дії керування.\"],\"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\":[\"Завантажити аватар\"],\"jUXib7\":[\"Повідомлення з відповіддю більше не відображається\"],\"jW5Uwh\":[\"Контролюйте завантаження зовнішніх медіа. Вимк / Безпечно / Надійні джерела / Весь вміст.\"],\"jXzms5\":[\"Параметри вкладення\"],\"jZlrte\":[\"Колір\"],\"jfC/xh\":[\"Контакт\"],\"jywMpv\":[\"#нова-назва-каналу\"],\"k112DD\":[\"Завантажити старіші повідомлення\"],\"k3ID0F\":[\"Фільтрувати учасників…\"],\"k65gsE\":[\"Детальний огляд\"],\"k7Zgob\":[\"Скасувати підключення\"],\"kAVx5h\":[\"Запрошень не знайдено\"],\"kCLEPU\":[\"Підключено до\"],\"kF5LKb\":[\"Ігноровані шаблони:\"],\"kG2fiE\":[\"визначений у конфігурації\"],\"kGeOx/\":[\"Приєднатися до \",[\"0\"]],\"kITKr8\":[\"Завантаження режимів каналу...\"],\"kPpPsw\":[\"Ви є IRC-оператором\"],\"kWJmRL\":[\"Ви\"],\"kfcRb0\":[\"Аватар\"],\"kjMqSj\":[\"Скопіювати JSON\"],\"krViRy\":[\"Натисніть для копіювання як JSON\"],\"ks71ra\":[\"Винятки\"],\"kw4lRv\":[\"Напів-оператор каналу\"],\"kxgIRq\":[\"Виберіть або додайте канал для початку.\"],\"ky2mw7\":[\"через @\",[\"0\"]],\"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\":[\"Оновити\"],\"lH+ed1\":[\"Очікування першого кроку…\"],\"lHy8N5\":[\"Завантаження більше каналів...\"],\"lasgrr\":[\"використано\"],\"lbpf14\":[\"Приєднатися до \",[\"value\"]],\"lf3MT4\":[\"Канал, який покинути (за замовчуванням — поточний)\"],\"lfFsZ4\":[\"Канали\"],\"lkNdiH\":[\"Ім'я акаунту\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Завантажити зображення\"],\"loQxaJ\":[\"Я повернувся\"],\"lvfaxv\":[\"ГОЛОВНА\"],\"m16xKo\":[\"Додати\"],\"m8flAk\":[\"Попередній перегляд (ще не завантажено)\"],\"mDkV0w\":[\"Запуск робочого процесу…\"],\"mEPxTp\":[\"<0>⚠️ Будьте обережні! Відкривайте лише посилання з надійних джерел. Шкідливі посилання можуть порушити вашу безпеку або конфіденційність.\"],\"mHGdhG\":[\"Інформація про сервер\"],\"mHS8lb\":[\"Повідомлення #\",[\"0\"]],\"mHfd/S\":[\"Що ви робите\"],\"mMYBD9\":[\"Широкий - ширша область захисту\"],\"mTGsPd\":[\"Тема каналу\"],\"mU8j6O\":[\"Без зовнішніх повідомлень (+n)\"],\"mZp8FL\":[\"Автоматичне повернення до одного рядка\"],\"mdQu8G\":[\"ВашПсевдонім\"],\"miSSBQ\":[\"Коментарі (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Користувач автентифікований\"],\"mwtcGl\":[\"Закрити коментарі\"],\"mzI/c+\":[\"Завантажити\"],\"n3fGRk\":[\"встановлено \",[\"0\"]],\"nE9jsU\":[\"Розслаблений - менш агресивний захист\"],\"nNflMD\":[\"Покинути канал\"],\"nPXkBi\":[\"Завантаження даних WHOIS...\"],\"nQnxxF\":[\"Повідомлення #\",[\"0\"],\" (Shift+Enter для нового рядка)\"],\"nWMRxa\":[\"Відкріпити\"],\"nX4XLG\":[\"Дії оператора\"],\"nkC032\":[\"Без профілю флуду\"],\"o69z4d\":[\"Надіслати попереджувальне повідомлення \",[\"username\"]],\"o9ylQi\":[\"Знайдіть GIF для початку\"],\"oFGkER\":[\"Повідомлення сервера\"],\"oOi11l\":[\"Прокрутити вниз\"],\"oPYIL5\":[\"мережа\"],\"oQEzQR\":[\"Нове DM\"],\"oXOSPE\":[\"Онлайн\"],\"oaTtrx\":[\"Пошук ботів\"],\"oal760\":[\"Можливі атаки «людина посередині» на з'єднання сервера\"],\"oeqmmJ\":[\"Надійні джерела\"],\"optX0N\":[[\"0\"],\" год тому\"],\"ovBPCi\":[\"За замовчуванням\"],\"p0Z69r\":[\"Шаблон не може бути порожнім\"],\"p1KgtK\":[\"Не вдалося завантажити аудіо\"],\"p59pEv\":[\"Детальніше\"],\"p7sRI6\":[\"Повідомляти інших, коли ви друкуєте\"],\"pBm1od\":[\"Секретний канал\"],\"pNmiXx\":[\"Ваш псевдонім за замовчуванням для всіх серверів\"],\"pQBYsE\":[\"Відповів у чаті\"],\"pUUo9G\":[\"Хост:\"],\"pVGPmz\":[\"Пароль акаунту\"],\"peNE68\":[\"Постійний\"],\"plhHQt\":[\"Немає даних\"],\"pm6+q5\":[\"Попередження про безпеку\"],\"pn5qSs\":[\"Додаткова інформація\"],\"q0cR4S\":[\"тепер відомий як **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Канал не відображатиметься у командах LIST або NAMES\"],\"qLpTm/\":[\"Видалити реакцію \",[\"emoji\"]],\"qVkGWK\":[\"Закріпити\"],\"qXgujk\":[\"Надіслати дію / емоцію\"],\"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\"],\" має такі проблеми безпеки:\"],\"sGH11W\":[\"Сервер\"],\"sHI1H+\":[\"тепер відомий як **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" запросив вас приєднатися до \",[\"channel\"]],\"sW5OjU\":[\"обов'язково\"],\"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\":[\"Зберегти\"],\"thC9Rq\":[\"Покинути канал\"],\"tiBsJk\":[\"покинув \",[\"channelName\"]],\"tt4/UD\":[\"вийшов (\",[\"reason\"],\")\"],\"tu2JEr\":[\"Канал для приєднання (#назва)\"],\"u0TcnO\":[\"Псевдонім {nick} вже використовується, повторна спроба з {newNick}\"],\"u0a8B4\":[\"Автентифікуватися як IRC-оператор для адміністративного доступу\"],\"u0rWFU\":[\"Створено після (хв тому)\"],\"u72w3t\":[\"Користувачі та шаблони для ігнорування\"],\"u7jc2L\":[\"вийшов\"],\"uAQUqI\":[\"Статус\"],\"uB85T3\":[\"Помилка збереження: \",[\"msg\"]],\"uMIUx8\":[\"Видалити бота \",[\"0\"],\"? Це м'яко видаляє рядок у базі даних; повторно використати нік можна лише після /REHASH.\"],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC сервери:\"],\"ukyW4o\":[\"Ваші запрошувальні посилання\"],\"usSSr/\":[\"Рівень масштабу\"],\"v7uvcf\":[\"Програма:\"],\"vE8kb+\":[\"Використовуйте Shift+Enter для нового рядка (Enter надсилає)\"],\"vERlcd\":[\"Профіль\"],\"vK0RL8\":[\"Без теми\"],\"vSJd18\":[\"Відео\"],\"vXIe7J\":[\"Мова\"],\"vaHYxN\":[\"Справжнє ім'я\"],\"vhjbKr\":[\"Відсутній\"],\"w4NYox\":[\"клієнт \",[\"title\"]],\"w8xQRx\":[\"Недійсне значення\"],\"wCKe3+\":[\"Історія робочих процесів\"],\"wFjjxZ\":[\"був кікнутий з \",[\"channelName\"],\" користувачем \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Винятків з заборон не знайдено\"],\"wPrGnM\":[\"Адміністратор каналу\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"Міркування\"],\"wbm86v\":[\"Відображати, коли користувачі входять або виходять з каналів\"],\"wdxz7K\":[\"Джерело\"],\"whqZ9r\":[\"Додаткові слова або фрази для виділення\"],\"wm7RV4\":[\"Звук сповіщення\"],\"wz/Yoq\":[\"Ваші повідомлення можуть бути перехоплені при передачі між серверами\"],\"x3+y8b\":[\"Стільки людей зареєструвалося через це посилання\"],\"xCJdfg\":[\"Очистити\"],\"xOTzt5\":[\"щойно\"],\"xUHRTR\":[\"Автоматично автентифікуватися як оператор при підключенні\"],\"xWHwwQ\":[\"Блокування\"],\"xYilR2\":[\"Медіа\"],\"xbi8D6\":[\"Цей сервер не підтримує запрошувальні посилання (можливість<0>obby.world/invitationне оголошена). Ви все ще можете нормально спілкуватися; ця панель призначена для мереж на основі obbyircd.\"],\"xceQrO\":[\"Підтримуються лише захищені websocket-з'єднання\"],\"xdtXa+\":[\"назва-каналу\"],\"xeiujy\":[\"Текст\"],\"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\"]],\"zPBDzU\":[\"Скасувати робочий процес\"],\"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..4fabec84 100644 --- a/src/locales/uk/messages.po +++ b/src/locales/uk/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - Привносимо IRC у майбутнє" msgid "— open in viewer" msgstr "— відкрити у переглядачі" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(обробляється ObsidianIRC)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +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); @@ -41,16 +49,41 @@ msgstr "{0, plural, one {Показати ще 1 елемент} few {Показ msgid "{0} and {1} are typing..." msgstr "{0} та {1} пишуть..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} обов'язковий." + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} пише..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} має бути числом." + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} має бути цілим числом." + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} старіших повідомлень" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} крок(ів)" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} крок(ів) очікує на затвердження" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "Розширені фільтри" msgid "Album" msgstr "Альбом" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "Усі" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "Весь вміст" @@ -297,6 +334,12 @@ msgstr "Застосувати фільтри та оновити" msgid "Applying..." msgstr "Застосовується..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "Затвердити" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "Автентифікований акаунт" msgid "Auto Fallback to Single Line" msgstr "Автоматичне повернення до одного рядка" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "Автоматичне закриття через {secondsLeft}с" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "Автоматично автентифікуватися як оператор при підключенні" @@ -359,6 +406,10 @@ msgstr "Відсутній" msgid "Away from keyboard" msgstr "Відійшов від клавіатури" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "Повідомлення про відсутність" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "Повідомлення про відсутність" msgid "Away:" msgstr "Відсутність:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "Блокування" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "Бот" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "Бот ще не зареєстрував жодної слеш-команди." + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "Боти" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "Боти в цій мережі" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "Переглянути всі канали на сервері" @@ -430,6 +499,7 @@ msgstr "Переглянути всі канали на сервері" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "Скасувати підключення" msgid "Cancel reply" msgstr "Скасувати відповідь" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "Скасувати робочий процес" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "Змінити назву каналу (лише оператори)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "Змінити свій нікнейм на цьому сервері" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "Змінено режими каналу" @@ -459,6 +537,7 @@ msgstr "Змінено псевдонім" msgid "changed the topic to: {topic}" msgstr "змінив тему на: {topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "Канал" @@ -519,6 +598,14 @@ msgstr "Власник каналу" msgid "Channel Settings" msgstr "Налаштування каналу" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "Канал для приєднання (#назва)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "Канал, який покинути (за замовчуванням — поточний)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "Тема каналу" @@ -531,6 +618,7 @@ msgstr "Канал не відображатиметься у командах L msgid "channel-name" msgstr "назва-каналу" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "Канали" @@ -606,6 +694,9 @@ msgstr "Ліміт клієнтів (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "Коментарі" msgid "Comments ({commentCount})" msgstr "Коментарі ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "визначений у конфігурації" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "Бот, визначений у конфігурації. Відредагуйте obbyircd.conf і виконайте /REHASH, щоб змінити стан." + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "Налаштуйте детальні правила захисту від флуду. Кожне правило вказує, яку активність відстежувати і які дії вживати при перевищенні порогів." @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "Стандартний псевдонім" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "Видалити" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "Видалити бота {0}? Це м'яко видаляє рядок у базі даних; повторно використати нік можна лише після /REHASH." + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "Видалити канал" @@ -843,6 +948,10 @@ msgstr "Дослідити" msgid "Discover the world of IRC with ObsidianIRC" msgstr "Відкрийте світ IRC з ObsidianIRC" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "Закрити" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "Відхилити сповіщення" @@ -1039,6 +1148,10 @@ msgstr "Фільтрувати канали..." msgid "Filter members…" msgstr "Фільтрувати учасників…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "Перше повідомлення для надсилання" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "Профіль флуду (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line (глобальний бан)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway підключено" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway у мережі" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "Загальне" @@ -1186,6 +1307,10 @@ msgstr "Зображення {0} з {1}" msgid "Image preview" msgstr "Попередній перегляд зображення" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "ВХІД" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "Інфо:" @@ -1270,6 +1395,10 @@ msgstr "Приєднатися до {0}" msgid "Join {value}" msgstr "Приєднатися до {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "Приєднатися до каналу" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "Дізнатися більше про власні правила →" msgid "Learn more about profiles →" msgstr "Дізнатися більше про профілі →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "Покинути канал" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "Покинути канал" @@ -1411,6 +1544,14 @@ msgstr "Керуйте інформацією профілю та метадан msgid "Mark as bot - usually 'on' or empty" msgstr "Позначити як бот - зазвичай 'on' або порожньо" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "Позначити себе як відсутнього" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "Позначити себе як повернувся" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "Макс. користувачів" @@ -1573,6 +1714,10 @@ msgstr "Назва мережі" msgid "New DM" msgstr "Нове DM" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "Новий нікнейм" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "Наступне зображення" @@ -1617,6 +1762,10 @@ msgstr "Винятків з заборон не знайдено" msgid "No bans found" msgstr "Заборон не знайдено" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "У цій мережі ще не зареєстровано жодного бота." + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "Без центрального сервера:" @@ -1672,7 +1821,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" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - Привносимо IRC у майбутнє" msgid "Off" msgstr "Вимк" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "не в мережі" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "Офлайн" @@ -1754,6 +1908,10 @@ msgstr "Офлайн" msgid "on" msgstr "увімк" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "один з:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "Ой! Розрив мережі! ⚠️" msgid "Op" msgstr "Оп" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "Відкрити приватне повідомлення з користувачем" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "Відкрити налаштування конфігурації каналу" @@ -1828,10 +1990,23 @@ msgstr "Пароль оператора" msgid "Oper Username" msgstr "Ім'я оператора" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "Дії оператора" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "Необов'язкові звіти про збої для покращення додатку" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "ВИХІД" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "вивід скорочено" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "Власник" @@ -1994,6 +2169,10 @@ msgstr "Повідомлення про вихід" msgid "Quit the server" msgstr "Вийти з сервера" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "виконав" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "React" @@ -2024,6 +2203,10 @@ msgstr "Причина" msgid "Reason (optional)" msgstr "Причина (необов'язково)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "Міркування" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "Перепідключитися до сервера" @@ -2037,6 +2220,11 @@ msgstr "Оновити" msgid "Register for an account" msgstr "Зареєструвати акаунт" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "Відхилити" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "Розслаблений" @@ -2077,11 +2265,27 @@ msgstr "Перейменувати цей канал на сервері. Усі msgid "Render markdown formatting in messages" msgstr "Відображати Markdown-форматування у повідомленнях" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "Знову відкрити робочий процес, який створив це повідомлення ({stepCount} кроків)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "Відповісти" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "обов'язково" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "Відповів у чаті" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "Повідомлення з відповіддю більше не відображається" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "Повторити надсилання" msgid "Rules" msgstr "Правила" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "Виконати" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "Безпечно" @@ -2121,6 +2329,10 @@ msgstr "Збережено" msgid "Saving..." msgstr "Збереження..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "Прокрутити чат до цієї відповіді" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "Прокрутити вниз" msgid "Search" msgstr "Пошук" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "Пошук ботів" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "Знайдіть GIF для початку" @@ -2183,6 +2399,10 @@ msgstr "Попередження про безпеку" msgid "Seek" msgstr "Перемотати" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "Виберіть бота ліворуч, щоб побачити його команди та дії керування." + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "Вибрати канал" @@ -2195,6 +2415,10 @@ msgstr "Вибрати учасника" msgid "Select or add a channel to get started." msgstr "Виберіть або додайте канал для початку." +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "самостійно зареєстрований" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "Надіслати GIF" msgid "Send a warning message to {username}" msgstr "Надіслати попереджувальне повідомлення {username}" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "Надіслати дію / емоцію" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "Надіслати запрошення" @@ -2280,6 +2508,10 @@ msgstr "Пароль сервера" msgid "Server-to-server communication may use unencrypted connections" msgstr "Зв'язок між серверами може використовувати незашифровані з'єднання" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "По всьому серверу" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "Встановити тему…" @@ -2376,6 +2608,10 @@ msgstr "Показує медіа з надійного файлового хо msgid "Signed On" msgstr "Час входу" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "Слеш-команди" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "Програма:" @@ -2392,10 +2628,18 @@ msgstr "Сортувати за кількістю користувачів" msgid "SoundCloud player" msgstr "Програвач SoundCloud" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "Джерело" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "Почати приватне повідомлення" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "Запуск робочого процесу…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "Статусні повідомлення" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "Зупинити" @@ -2419,10 +2664,18 @@ msgstr "Строгий" msgid "Strict - More aggressive protection" msgstr "Строгий - більш агресивний захист" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "Призупинити" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "Система" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "Текст" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "Текстові канали" @@ -2447,6 +2700,14 @@ msgstr "Тема, яка відображатиметься для цього к msgid "The user will receive an invitation to join {channelName}." msgstr "Користувач отримає запрошення приєднатися до {channelName}." +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "Робочий процес, який створив це повідомлення, більше не перебуває у стані" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "Ця команда не приймає параметрів." + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "Це поле обов'язкове" @@ -2510,6 +2771,10 @@ msgstr "Перемкнути сповіщення" msgid "Toggle search" msgstr "Перемкнути пошук" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "Інструмент" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "Тему встановлено після (хв тому)" @@ -2527,6 +2792,10 @@ msgstr "Тема:" msgid "Total: {0}" msgstr "Всього: {0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "Транспорт" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "Надійні джерела" @@ -2561,6 +2830,10 @@ msgstr "Відкріпити приватну розмову" msgid "Unpin this private message conversation" msgstr "Відкріпити цю приватну розмову" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "Відновити" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "Завантажити" @@ -2687,6 +2960,11 @@ msgstr "Дуже розслаблений" msgid "Very Strict" msgstr "Дуже строгий" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "через @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "Відео" @@ -2730,6 +3008,10 @@ msgstr "Користувач з голосом" msgid "Volume" msgstr "Гучність" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "Очікування першого кроку…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "Виключено з каналу" msgid "We don't store your IRC communications on our servers" msgstr "Ми не зберігаємо ваші IRC-комунікації на наших серверах" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "Ласкаво просимо до {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "Що ти зараз робиш?" msgid "What this means:" msgstr "Що це означає:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "Що ви робите" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "Що у вас на думці?" @@ -2780,6 +3070,10 @@ msgstr "Що у вас на думці?" msgid "WHISPER" msgstr "ШЕПІТ" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "Шепнути користувачу в контексті поточного каналу" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "Широкий - ширша область захисту" @@ -2788,6 +3082,19 @@ msgstr "Широкий - ширша область захисту" msgid "Will default to 'no reason' if left empty" msgstr "За замовчуванням буде 'без причини', якщо залишити порожнім" +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "Історія робочих процесів" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "Історія робочих процесів ({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "Виконується…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/zh-TW/messages.mjs b/src/locales/zh-TW/messages.mjs index 12dd39cf..68f71f47 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>风险: 敏感信息(消息、私人对话、身份验证详情)可能会暴露给网络管理员或位于 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\"],\"\"],\"chQsxg\":[\"複製格式化輸出\"],\"cl/A5J\":[\"欢迎来到 \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"删除\"],\"coPLXT\":[\"我們不在伺服器上儲存您的 IRC 通訊\"],\"crYH/6\":[\"SoundCloud 播放器\"],\"d3sis4\":[\"添加服务器\"],\"d9aN5k\":[\"将 \",[\"username\"],\" 移出频道\"],\"dEgA5A\":[\"取消\"],\"dGi1We\":[\"取消置顶此私信对话\"],\"dJVuyC\":[\"離開了 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"至\"],\"dXqxlh\":[\"<0>⚠️ 安全风险! 此连接可能容易遭受窃听或中间人攻击。\"],\"da9Q/R\":[\"已更改频道模式\"],\"dhJN3N\":[\"顯示評論\"],\"dj2xTE\":[\"关闭通知\"],\"dpCzmC\":[\"洪水保護設定\"],\"e9dQpT\":[\"是否在新标签页中打开此链接?\"],\"ePK91l\":[\"编辑\"],\"eYBDuB\":[\"上傳圖片或提供含可選 \",[\"size\"],\" 替換的 URL\"],\"edBbee\":[\"通过 hostmask 封禁 \",[\"username\"],\"(阻止其从相同 IP/主机重新加入)\"],\"ekfzWq\":[\"使用者設定\"],\"elPDWs\":[\"自定义您的 IRC 客户端体验\"],\"eu2osY\":[\"<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>⚠️ 请注意! 仅打开来自可信来源的链接。恶意链接可能危害您的安全或隐私。\"],\"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\"],\" 的连接存在以下安全问题:\"],\"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/invitation功能)。您仍可正常聊天;此面板僅適用於採用 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\":[\"標記為離開時的預設訊息\"],\"+fRR7i\":[\"停用\"],\"+mVPqU\":[\"在訊息中渲染 Markdown 格式\"],\"+vqCJH\":[\"您的帳戶認證使用者名稱\"],\"+yPBXI\":[\"選擇檔案\"],\"+zy2Nq\":[\"類型\"],\"/09cao\":[\"链路安全级别较低(级别 \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"將自己標記為回來\"],\"/3BQ4J\":[\"頻道外的使用者無法傳送訊息\"],\"/4C8U0\":[\"全部複製\"],\"/6BzZF\":[\"切换成员列表\"],\"/AkXyp\":[\"確認?\"],\"/TNOPk\":[\"使用者已離開\"],\"/XQgft\":[\"探索\"],\"/cF7Rs\":[\"音量\"],\"/dqduX\":[\"下一页\"],\"/fc3q4\":[\"所有内容\"],\"/kISDh\":[\"啟用通知聲音\"],\"/n04sB\":[\"踢出\"],\"/rTz0M\":[\"音訊\"],\"/rfkZe\":[\"提及和訊息時播放聲音\"],\"/xQ19T\":[\"此網路上的機器人\"],\"0/0ZGA\":[\"頻道名稱遮罩\"],\"0D6j7U\":[\"了解更多自訂規則 →\"],\"0XsHcR\":[\"踢出用户\"],\"0ZpE//\":[\"依使用者數排序\"],\"0bEPwz\":[\"设置为离开\"],\"0dGkPt\":[\"展开频道列表\"],\"0gS7M5\":[\"显示名称\"],\"0kS+M8\":[\"範例網路\"],\"0rgoY7\":[\"僅連線至您選擇的伺服器\"],\"0wdd7X\":[\"加入\"],\"0wkVYx\":[\"私人訊息\"],\"111uHX\":[\"链接预览\"],\"196EG4\":[\"删除私聊\"],\"1C/fOn\":[\"機器人尚未註冊任何斜線命令。\"],\"1DSr1i\":[\"注册账户\"],\"1O/24y\":[\"切换频道列表\"],\"1QfxQT\":[\"關閉\"],\"1VPJJ2\":[\"外部链接警告\"],\"1ZC/dv\":[\"沒有未讀提及或訊息\"],\"1pO1zi\":[\"服务器名称为必填项\"],\"1t/NnN\":[\"拒絕\"],\"1uwfzQ\":[\"查看频道主题\"],\"268g7c\":[\"输入显示名称\"],\"2F9+AZ\":[\"尚未擷取任何原始 IRC 流量。請嘗試連線或傳送訊息。\"],\"2FOFq1\":[\"网络上的服务器管理员可能读取您的消息\"],\"2FYpfJ\":[\"更多\"],\"2HF1Y2\":[[\"inviter\"],\" 邀請了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"2I70QL\":[\"查看用户资料信息\"],\"2QYdmE\":[\"用戶:\"],\"2QpEjG\":[\"已離開\"],\"2YE223\":[\"发消息到 #\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"2bimFY\":[\"使用服务器密码\"],\"2iTmdZ\":[\"本機儲存:\"],\"2odkwe\":[\"嚴格 – 更強的保護\"],\"2uDhbA\":[\"输入要邀请的用户名\"],\"2xXP/g\":[\"加入頻道\"],\"2ygf/L\":[\"← 返回\"],\"2zEgxj\":[\"搜索 GIF...\"],\"3JjdaA\":[\"執行\"],\"3NJ4MW\":[\"重新開啟產生此訊息的工作流程(\",[\"stepCount\"],\" 個步驟)\"],\"3RdPhl\":[\"重命名频道\"],\"3THokf\":[\"有發言權的使用者\"],\"3TSz9S\":[\"最小化\"],\"3et0TM\":[\"將聊天捲動至此回覆\"],\"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\":[\"短訊息自動切換至單行\"],\"4uKgKr\":[\"傳出\"],\"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\":[\"有隱私問題?請聯絡我們:\"],\"9q17ZR\":[[\"0\"],\" 為必填項。\"],\"9qIYMn\":[\"新暱稱\"],\"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\":[\"您有未保存的更改。确定要关闭而不保存吗?\"],\"BIDT9R\":[\"機器人\"],\"BIf9fi\":[\"您的狀態訊息\"],\"BPm98R\":[\"尚未選擇伺服器。請先從側邊欄選擇一個伺服器;邀請連結是依伺服器管理的。\"],\"BZz3md\":[\"您的個人網站\"],\"Bgm/H7\":[\"允許輸入多行文字\"],\"BiQIl1\":[\"置顶此私信对话\"],\"BlNZZ2\":[\"点击跳转到消息\"],\"Bowq3c\":[\"只有管理員可以更改頻道主題\"],\"Btozzp\":[\"此图片已过期\"],\"Bycfjm\":[\"總計:\",[\"0\"]],\"C6IBQc\":[\"複製完整 JSON\"],\"C9L9wL\":[\"資料收集\"],\"CDq4wC\":[\"管理用户\"],\"CHVRxG\":[\"发消息给 @\",[\"0\"],\"(Shift+Enter 换行)\"],\"CN9zdR\":[\"Oper 用户名和密码为必填项\"],\"CW3sYa\":[\"添加表情 \",[\"emoji\"]],\"CaAkqd\":[\"顯示退出事件\"],\"CaQ1Gb\":[\"由設定定義的機器人。編輯 obbyircd.conf 並執行 /REHASH 以變更狀態。\"],\"CbvaYj\":[\"依暱稱封禁\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"选择频道\"],\"CsekCi\":[\"普通\"],\"D+NlUC\":[\"系统\"],\"D28t6+\":[\"已加入並退出\"],\"DB8zMK\":[\"套用\"],\"DBcWHr\":[\"自訂通知音效檔\"],\"DSHF2K\":[\"產生此訊息的工作流程已不在狀態中\"],\"DTy9Xw\":[\"媒體預覽\"],\"Dj4pSr\":[\"选择一个安全密码\"],\"Du+zn+\":[\"搜尋中...\"],\"Du2T2f\":[\"未找到設定\"],\"DwsSVQ\":[\"套用篩選並重新整理\"],\"E3W/zd\":[\"預設暱稱\"],\"E6nRW7\":[\"复制链接\"],\"E703RG\":[\"模式:\"],\"EAeu1Z\":[\"傳送邀請\"],\"EFKJQT\":[\"設定\"],\"EGPQBv\":[\"自訂洪水規則 (+f)\"],\"ELik0r\":[\"查看完整隱私權政策\"],\"EPbeC2\":[\"查看或编辑频道主题\"],\"EQCDNT\":[\"輸入 oper 用戶名...\"],\"EUvulZ\":[\"找到 1 則符合「\",[\"searchQuery\"],\"」的訊息\"],\"EatZYJ\":[\"下一张图片\"],\"EdQY6l\":[\"無\"],\"EnqLYU\":[\"搜索服务器...\"],\"Eu7YKa\":[\"自助註冊\"],\"F0OKMc\":[\"编辑服务器\"],\"F6Int2\":[\"啟用醒目提示\"],\"FDoLyE\":[\"最多使用者數\"],\"FUU/hZ\":[\"控制聊天中載入的外部媒體數量。\"],\"Fdp03t\":[\"開啟\"],\"FfPWR0\":[\"弹窗\"],\"FjkaiT\":[\"缩小\"],\"FlqOE9\":[\"这意味着:\"],\"FolHNl\":[\"管理您的账户和身份验证\"],\"Fp2Dif\":[\"已退出服务器\"],\"G5KmCc\":[\"GZ-Line(全域 Z-Line)\"],\"GDs0lz\":[\"<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\"],\" 正在输入...\"],\"IcHxhR\":[\"離線\"],\"IgrLD/\":[\"暫停\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"回复\"],\"IoHMnl\":[\"最大值為 \",[\"0\"]],\"IvMj+0\":[\"管理员\"],\"J28zul\":[\"正在連線...\"],\"J5T9NW\":[\"使用者資訊\"],\"J8Y5+z\":[\"哎呀!網路分裂!⚠️\"],\"JBHkBA\":[\"已离开频道\"],\"JCwL0Q\":[\"输入原因(可选)\"],\"JFciKP\":[\"切换\"],\"JMXMCX\":[\"離開訊息\"],\"JXGkhG\":[\"更改频道名称(仅限管理员)\"],\"JYiL1b\":[\"其中之一:\"],\"JcD7qf\":[\"更多操作\"],\"JdkA+c\":[\"隱密 (+s)\"],\"Jmu12l\":[\"服务器频道\"],\"JvQ++s\":[\"啟用 Markdown\"],\"K2jwh/\":[\"無可用 WHOIS 資料\"],\"K4vEhk\":[\"(已停用)\"],\"KAXSwC\":[\"语音权限\"],\"KDfTdX\":[\"删除消息\"],\"KKBlUU\":[\"嵌入\"],\"KM0pLb\":[\"欢迎来到本频道!\"],\"KR6W2h\":[\"取消忽略使用者\"],\"KV+Bi1\":[\"僅限邀請 (+i)\"],\"KdCtwE\":[\"在重置計數器之前監控洪水活動的秒數\"],\"Kkezga\":[\"服务器密码\"],\"KsiQ/8\":[\"使用者必須受邀才能加入頻道\"],\"KtADxr\":[\"執行了\"],\"L+gB/D\":[\"頻道資訊\"],\"LC1a7n\":[\"IRC 服务器报告其服务器间链路的安全级别较低。这意味着当您的消息在网络中的 IRC 服务器之间转发时,可能未经过妥善加密,或者 SSL/TLS 证书未被正确验证。\"],\"LN3RO2\":[[\"0\"],\" 個步驟等待核准\"],\"LNfLR5\":[\"顯示踢出事件\"],\"LQb0W/\":[\"顯示所有事件\"],\"LU7/yA\":[\"顯示用的別名,可包含空格、表情符號和特殊字元。真實頻道名(\",[\"channelName\"],\")仍用於 IRC 指令。\"],\"LUb9O7\":[\"需要有效的服务器端口\"],\"LV4fT6\":[\"描述(選填,例如「第三季 Beta 測試者」)\"],\"LYzbQ2\":[\"工具\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"隱私權政策\"],\"LcuSDR\":[\"管理您的个人资料信息和元数据\"],\"LqLS9B\":[\"顯示暱稱變更\"],\"LsDQt2\":[\"频道设置\"],\"LtI9AS\":[\"频道所有者\"],\"LuNhhL\":[\"对此消息做出了回应\"],\"M/AZNG\":[\"頭像圖片 URL\"],\"M/WIer\":[\"傳送訊息\"],\"M45wtf\":[\"此命令不需要參數。\"],\"M8er/5\":[\"名稱:\"],\"MHk+7g\":[\"上一张图片\"],\"MRorGe\":[\"私信用户\"],\"MVbSGP\":[\"時間視窗(秒)\"],\"MkpcsT\":[\"您的訊息和設定儲存在本機裝置上\"],\"N/hDSy\":[\"標記為機器人——通常為 'on' 或空白\"],\"N40H+G\":[\"全部\"],\"N7TQbE\":[\"邀請使用者加入 \",[\"channelName\"]],\"NCca/o\":[\"輸入預設暱稱...\"],\"NQN2HS\":[\"取消停用\"],\"Nqs6B9\":[\"显示所有外部媒体。任何 URL 都可能向未知服务器发送请求。\"],\"Nt+9O7\":[\"使用 WebSocket 而非原始 TCP\"],\"NxIHzc\":[\"踢出用戶\"],\"O+HhhG\":[\"在目前頻道情境中對使用者悄悄話\"],\"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\"]],\"RlCInP\":[\"斜線命令\"],\"RySp6q\":[\"隱藏評論\"],\"RzfkXn\":[\"在此伺服器上更改你的暱稱\"],\"SPKQTd\":[\"昵称为必填项\"],\"SPVjfj\":[\"留空将默认显示\\\"无原因\\\"\"],\"SQKPvQ\":[\"邀请用户\"],\"SkZcl+\":[\"選擇預定義的洪水保護設定檔。這些設定檔為不同使用場景提供均衡的保護設定。\"],\"Slr+3C\":[\"最少使用者數\"],\"Spnlre\":[\"您邀請了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"T/ckN5\":[\"在查看器中打开\"],\"T91vKp\":[\"播放\"],\"TImSWn\":[\"(由 ObsidianIRC 處理)\"],\"TRDppN\":[\"Webhook\"],\"TV2Wdu\":[\"了解我們如何處理您的資料並保護您的隱私。\"],\"TgFpwD\":[\"正在套用...\"],\"TkzSFB\":[\"无更改\"],\"TtserG\":[\"输入真实姓名\"],\"Ttz9J1\":[\"輸入密碼...\"],\"Tz0i8g\":[\"設定\"],\"U3pytU\":[\"管理员\"],\"U7yg75\":[\"將自己標記為離開\"],\"UDb2YD\":[\"添加表情\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"您尚未建立任何邀請連結。請使用上方表單建立第一個。\"],\"UGT5vp\":[\"儲存設定\"],\"UV5hLB\":[\"未找到封禁\"],\"Uaj3Nd\":[\"状态消息\"],\"Ue3uny\":[\"預設(無設定檔)\"],\"UkARhe\":[\"普通 – 標準保護\"],\"Umn7Cj\":[\"尚無評論,成為第一個吧!\"],\"UqtiKk\":[[\"secondsLeft\"],\" 秒後自動關閉\"],\"UrEy4W\":[\"開啟與使用者的私訊\"],\"UtUIRh\":[[\"0\"],\" 則較早的訊息\"],\"UwzP+U\":[\"安全連線\"],\"V0/A4O\":[\"頻道擁有者\"],\"V2dwib\":[[\"0\"],\" 必須是數字。\"],\"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\":[\"取消置顶私聊\"],\"XklovM\":[\"處理中…\"],\"Xm/s+u\":[\"顯示\"],\"Xp2n93\":[\"显示来自服务器受信任文件主机的媒体。不会向外部服务发送任何请求。\"],\"XvjC4F\":[\"正在儲存...\"],\"Y+tK3n\":[\"要傳送的第一則訊息\"],\"Y/qryO\":[\"未找到与搜索条件匹配的用户\"],\"YAqRpI\":[\"帳戶 \",[\"account\"],\" 註冊成功:\",[\"message\"]],\"YBXJ7j\":[\"傳入\"],\"YEfzvP\":[\"受保護主題 (+t)\"],\"YQOn6a\":[\"收起成员列表\"],\"YRCoE9\":[\"頻道操作員\"],\"YURQaF\":[\"查看個人資料\"],\"YdBSvr\":[\"控制媒体显示和外部内容\"],\"Yj6U3V\":[\"無中央伺服器:\"],\"YjvpGx\":[\"代词\"],\"YqH4l4\":[\"无密钥\"],\"YyUPpV\":[\"帳戶:\"],\"Z7ZXbT\":[\"核准\"],\"ZJSWfw\":[\"中斷伺服器連線時顯示的訊息\"],\"ZR1dJ4\":[\"邀請\"],\"ZdWg0V\":[\"在浏览器中打开\"],\"ZhRBbl\":[\"搜索消息…\"],\"Zmcu3y\":[\"進階篩選\"],\"ZqLD8l\":[\"伺服器範圍\"],\"a2/8e5\":[\"主題設定時間晚於(分鐘前)\"],\"aHKcKc\":[\"上一页\"],\"aJTbXX\":[\"Oper 密码\"],\"aP9gNu\":[\"輸出已截斷\"],\"aQryQv\":[\"模式已存在\"],\"aW9pLN\":[\"頻道允許的最大使用者數,留空表示無限制。\"],\"ah4fmZ\":[\"同时显示来自 YouTube、Vimeo、SoundCloud 及类似知名服务的预览。\"],\"aifXak\":[\"此頻道沒有媒體\"],\"ap2zBz\":[\"宽松\"],\"az8lvo\":[\"关\"],\"azXSNo\":[\"展开成员列表\"],\"azdliB\":[\"登录账户\"],\"b26wlF\":[\"她/她的\"],\"bD/+Ei\":[\"严格\"],\"bFDO8z\":[\"gateway 上線\"],\"bQ6BJn\":[\"設定詳細的洪水保護規則。每條規則指定要監控的活動類型以及超過閾值時採取的操作。\"],\"bVBC/W\":[\"Gateway 已連線\"],\"beV7+y\":[\"使用者將收到加入 \",[\"channelName\"],\" 的邀請。\"],\"bk84cH\":[\"离开消息\"],\"bkHdLj\":[\"添加 IRC 服务器\"],\"bmQLn5\":[\"新增規則\"],\"bv4cFj\":[\"傳輸\"],\"bwRvnp\":[\"操作\"],\"c8+EVZ\":[\"已验证账户\"],\"cGYUlD\":[\"未加载任何媒体预览。\"],\"cLF98o\":[\"顯示評論 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"没有可用用户\"],\"cSgpoS\":[\"置顶私聊\"],\"cde3ce\":[\"发消息给 <0>\",[\"0\"],\"\"],\"chQsxg\":[\"複製格式化輸出\"],\"cl/A5J\":[\"欢迎来到 \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"删除\"],\"coPLXT\":[\"我們不在伺服器上儲存您的 IRC 通訊\"],\"crYH/6\":[\"SoundCloud 播放器\"],\"d3sis4\":[\"添加服务器\"],\"d9aN5k\":[\"将 \",[\"username\"],\" 移出频道\"],\"dEgA5A\":[\"取消\"],\"dGi1We\":[\"取消置顶此私信对话\"],\"dJVuyC\":[\"離開了 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"至\"],\"dRqrdL\":[[\"0\"],\" 必須是整數。\"],\"dXqxlh\":[\"<0>⚠️ 安全风险! 此连接可能容易遭受窃听或中间人攻击。\"],\"da9Q/R\":[\"已更改频道模式\"],\"dhJN3N\":[\"顯示評論\"],\"dj2xTE\":[\"关闭通知\"],\"dnUmOX\":[\"此網路上尚未註冊任何機器人。\"],\"dpCzmC\":[\"洪水保護設定\"],\"e7KzRG\":[[\"0\"],\" 個步驟\"],\"e9dQpT\":[\"是否在新标签页中打开此链接?\"],\"ePK91l\":[\"编辑\"],\"eYBDuB\":[\"上傳圖片或提供含可選 \",[\"size\"],\" 替換的 URL\"],\"edBbee\":[\"通过 hostmask 封禁 \",[\"username\"],\"(阻止其从相同 IP/主机重新加入)\"],\"ekfzWq\":[\"使用者設定\"],\"elPDWs\":[\"自定义您的 IRC 客户端体验\"],\"eu2osY\":[\"<0>💡 建议: 仅在您信任此服务器并了解相关风险的情况下继续操作。避免通过此连接共享敏感信息或密码。\"],\"euEhbr\":[\"點擊加入 \",[\"channel\"]],\"ez3vLd\":[\"啟用多行輸入\"],\"f0J5Ki\":[\"服务器之间的通信可能使用未加密的连接\"],\"f9BHJk\":[\"警告用户\"],\"fDOLLd\":[\"未找到頻道。\"],\"fYdEvu\":[\"工作流程歷史(\",[\"0\"],\")\"],\"ffzDkB\":[\"匿名分析:\"],\"fq1GF9\":[\"顯示使用者中斷伺服器連線的事件\"],\"gEF57C\":[\"此伺服器僅支援一種連線類型\"],\"gJuLUI\":[\"忽略清單\"],\"gNzMrk\":[\"目前頭像\"],\"gjPWyO\":[\"輸入暱稱...\"],\"gz6UQ3\":[\"最大化\"],\"h6razj\":[\"排除頻道名稱遮罩\"],\"hG6jnw\":[\"未设置主题\"],\"hG89Ed\":[\"圖片\"],\"hYgDIe\":[\"建立\"],\"hZ6znB\":[\"端口\"],\"ha+Bz5\":[\"例如:100:1440\"],\"hctjqj\":[\"在左側選擇一個機器人以檢視其命令和管理操作。\"],\"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\":[\"上傳頭像\"],\"jUXib7\":[\"回覆訊息已不在檢視範圍內\"],\"jW5Uwh\":[\"控制載入的外部媒體量。關閉 / 安全 / 可信來源 / 所有內容。\"],\"jXzms5\":[\"附件选项\"],\"jZlrte\":[\"颜色\"],\"jfC/xh\":[\"聯絡方式\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"載入較早的訊息\"],\"k3ID0F\":[\"筛选成员…\"],\"k65gsE\":[\"深入查看\"],\"k7Zgob\":[\"取消连接\"],\"kAVx5h\":[\"未找到邀請\"],\"kCLEPU\":[\"已連線至\"],\"kF5LKb\":[\"已忽略的模式:\"],\"kG2fiE\":[\"設定定義\"],\"kGeOx/\":[\"加入 \",[\"0\"]],\"kITKr8\":[\"正在載入頻道模式...\"],\"kPpPsw\":[\"您是 IRC Operator\"],\"kWJmRL\":[\"您\"],\"kfcRb0\":[\"头像\"],\"kjMqSj\":[\"複製 JSON\"],\"krViRy\":[\"點擊以 JSON 格式複製\"],\"ks71ra\":[\"例外\"],\"kw4lRv\":[\"頻道半操作員\"],\"kxgIRq\":[\"选择或添加频道以开始使用。\"],\"ky2mw7\":[\"透過 @\",[\"0\"]],\"ky6dWe\":[\"头像预览\"],\"l+GxCv\":[\"正在載入頻道...\"],\"l+IUVW\":[\"帳戶 \",[\"account\"],\" 驗證成功:\",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[\"已重新連線 \",[\"reconnectCount\"],\" 次\"]}]],\"l1l8sj\":[[\"0\"],\" 天前\"],\"l5NhnV\":[\"#頻道(選填)\"],\"l5jmzx\":[[\"0\"],\" 和 \",[\"1\"],\" 正在输入...\"],\"lCF0wC\":[\"重新整理\"],\"lH+ed1\":[\"正在等待第一個步驟…\"],\"lHy8N5\":[\"正在載入更多頻道...\"],\"lasgrr\":[\"已使用\"],\"lbpf14\":[\"加入 \",[\"value\"]],\"lf3MT4\":[\"要離開的頻道 (預設為目前頻道)\"],\"lfFsZ4\":[\"頻道\"],\"lkNdiH\":[\"帳戶名稱\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"上传图片\"],\"loQxaJ\":[\"我回来了\"],\"lvfaxv\":[\"首頁\"],\"m16xKo\":[\"新增\"],\"m8flAk\":[\"預覽(尚未上傳)\"],\"mDkV0w\":[\"正在啟動工作流程…\"],\"mEPxTp\":[\"<0>⚠️ 请注意! 仅打开来自可信来源的链接。恶意链接可能危害您的安全或隐私。\"],\"mHGdhG\":[\"伺服器資訊\"],\"mHS8lb\":[\"发消息到 #\",[\"0\"]],\"mHfd/S\":[\"你正在做什麼\"],\"mMYBD9\":[\"寬泛 – 更廣的保護範圍\"],\"mTGsPd\":[\"頻道主題\"],\"mU8j6O\":[\"禁止外部訊息 (+n)\"],\"mZp8FL\":[\"自動回退至單行\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"評論 (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"用户已认证\"],\"mwtcGl\":[\"关闭评论\"],\"mzI/c+\":[\"下载\"],\"n3fGRk\":[\"由 \",[\"0\"],\" 設定\"],\"nE9jsU\":[\"寬鬆 – 較弱的保護\"],\"nNflMD\":[\"离开频道\"],\"nPXkBi\":[\"正在載入 WHOIS 資料...\"],\"nQnxxF\":[\"发消息到 #\",[\"0\"],\"(Shift+Enter 换行)\"],\"nWMRxa\":[\"取消置顶\"],\"nX4XLG\":[\"操作員操作\"],\"nkC032\":[\"无洪水防护配置\"],\"o69z4d\":[\"向 \",[\"username\"],\" 发送警告消息\"],\"o9ylQi\":[\"搜尋 GIF 以開始\"],\"oFGkER\":[\"服务器通知\"],\"oOi11l\":[\"滚动到底部\"],\"oPYIL5\":[\"網路\"],\"oQEzQR\":[\"新私訊\"],\"oXOSPE\":[\"在线\"],\"oaTtrx\":[\"搜尋機器人\"],\"oal760\":[\"服务器链路可能遭受中间人攻击\"],\"oeqmmJ\":[\"受信任来源\"],\"optX0N\":[[\"0\"],\" 小時前\"],\"ovBPCi\":[\"默认\"],\"p0Z69r\":[\"模式不能為空\"],\"p1KgtK\":[\"音频加载失败\"],\"p59pEv\":[\"更多詳情\"],\"p7sRI6\":[\"讓其他人知道您正在輸入\"],\"pBm1od\":[\"秘密频道\"],\"pNmiXx\":[\"所有伺服器的預設暱稱\"],\"pQBYsE\":[\"已在聊天中回覆\"],\"pUUo9G\":[\"主機名:\"],\"pVGPmz\":[\"账户密码\"],\"peNE68\":[\"永久\"],\"plhHQt\":[\"無資料\"],\"pm6+q5\":[\"安全警告\"],\"pn5qSs\":[\"其他資訊\"],\"q0cR4S\":[\"現在稱為 **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"頻道不會出現在 LIST 或 NAMES 指令中\"],\"qLpTm/\":[\"移除表情 \",[\"emoji\"]],\"qVkGWK\":[\"置顶\"],\"qXgujk\":[\"傳送動作 / 表情\"],\"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\"],\" 的连接存在以下安全问题:\"],\"sGH11W\":[\"伺服器\"],\"sHI1H+\":[\"現在稱為 **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" 邀請您加入 \",[\"channel\"]],\"sW5OjU\":[\"必填\"],\"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\":[\"保存\"],\"thC9Rq\":[\"離開頻道\"],\"tiBsJk\":[\"離開了 \",[\"channelName\"]],\"tt4/UD\":[\"退出了 (\",[\"reason\"],\")\"],\"tu2JEr\":[\"要加入的頻道 (#name)\"],\"u0TcnO\":[\"暱稱 {nick} 已被使用,正在用 {newNick} 重試\"],\"u0a8B4\":[\"以 IRC 操作員身分進行管理存取認證\"],\"u0rWFU\":[\"建立時間晚於(分鐘前)\"],\"u72w3t\":[\"要忽略的使用者和模式\"],\"u7jc2L\":[\"退出了\"],\"uAQUqI\":[\"状态\"],\"uB85T3\":[\"儲存失敗:\",[\"msg\"]],\"uMIUx8\":[\"刪除機器人 \",[\"0\"],\"? 這將軟刪除資料庫記錄;稍後只有在執行 /REHASH 後才能重新使用該暱稱。\"],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC 伺服器:\"],\"ukyW4o\":[\"您的邀請連結\"],\"usSSr/\":[\"缩放级别\"],\"v7uvcf\":[\"軟體:\"],\"vE8kb+\":[\"Shift+Enter 換行(Enter 傳送)\"],\"vERlcd\":[\"个人资料\"],\"vK0RL8\":[\"無主題\"],\"vSJd18\":[\"影片\"],\"vXIe7J\":[\"语言\"],\"vaHYxN\":[\"真实姓名\"],\"vhjbKr\":[\"离开\"],\"w4NYox\":[[\"title\"],\" 客戶端\"],\"w8xQRx\":[\"值無效\"],\"wCKe3+\":[\"工作流程歷史\"],\"wFjjxZ\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"未找到封禁例外\"],\"wPrGnM\":[\"頻道管理員\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"推理\"],\"wbm86v\":[\"顯示使用者加入或離開頻道的事件\"],\"wdxz7K\":[\"來源\"],\"whqZ9r\":[\"要醒目提示的額外詞語或短語\"],\"wm7RV4\":[\"通知聲音\"],\"wz/Yoq\":[\"您的消息在服务器之间转发时可能被截获\"],\"x3+y8b\":[\"已透過此連結註冊的人數\"],\"xCJdfg\":[\"清除\"],\"xOTzt5\":[\"剛剛\"],\"xUHRTR\":[\"連線時自動以操作員身分認證\"],\"xWHwwQ\":[\"封禁\"],\"xYilR2\":[\"媒体\"],\"xbi8D6\":[\"此伺服器不支援邀請連結(未公告<0>obby.world/invitation功能)。您仍可正常聊天;此面板僅適用於採用 obbyircd 的網路。\"],\"xceQrO\":[\"仅支持安全的 WebSocket 连接\"],\"xdtXa+\":[\"頻道名稱\"],\"xeiujy\":[\"文字\"],\"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\"]],\"zPBDzU\":[\"取消工作流程\"],\"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..0e991107 100644 --- a/src/locales/zh-TW/messages.po +++ b/src/locales/zh-TW/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - 將 IRC 帶入未來" msgid "— open in viewer" msgstr "— 在檢視器中開啟" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(由 ObsidianIRC 處理)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +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); @@ -41,16 +49,41 @@ msgstr "{0, plural, other {顯示另外 {1} 個項目}}" msgid "{0} and {1} are typing..." msgstr "{0} 和 {1} 正在输入..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} 為必填項。" + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} 正在输入..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} 必須是數字。" + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} 必須是整數。" + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} 則較早的訊息" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} 個步驟" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} 個步驟等待核准" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "進階篩選" msgid "Album" msgstr "相簿" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "全部" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "所有内容" @@ -297,6 +334,12 @@ msgstr "套用篩選並重新整理" msgid "Applying..." msgstr "正在套用..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "核准" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "已驗證帳戶" msgid "Auto Fallback to Single Line" msgstr "自動回退至單行" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "{secondsLeft} 秒後自動關閉" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "連線時自動以操作員身分認證" @@ -359,6 +406,10 @@ msgstr "离开" msgid "Away from keyboard" msgstr "暂时离开" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "離開訊息" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "离开消息" msgid "Away:" msgstr "離開:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "封禁" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "机器人" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "機器人尚未註冊任何斜線命令。" + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "機器人" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "此網路上的機器人" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "浏览服务器上的所有频道" @@ -430,6 +499,7 @@ msgstr "浏览服务器上的所有频道" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "取消连接" msgid "Cancel reply" msgstr "取消回复" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "取消工作流程" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "更改频道名称(仅限管理员)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "在此伺服器上更改你的暱稱" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "已更改频道模式" @@ -459,6 +537,7 @@ msgstr "已更改昵称" msgid "changed the topic to: {topic}" msgstr "將主題更改為:{topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "頻道" @@ -519,6 +598,14 @@ msgstr "頻道擁有者" msgid "Channel Settings" msgstr "频道设置" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "要加入的頻道 (#name)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "要離開的頻道 (預設為目前頻道)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "頻道主題" @@ -531,6 +618,7 @@ msgstr "頻道不會出現在 LIST 或 NAMES 指令中" msgid "channel-name" msgstr "頻道名稱" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "頻道" @@ -606,6 +694,9 @@ msgstr "用戶端限制 (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "評論" msgid "Comments ({commentCount})" msgstr "評論 ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "設定定義" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "由設定定義的機器人。編輯 obbyircd.conf 並執行 /REHASH 以變更狀態。" + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "設定詳細的洪水保護規則。每條規則指定要監控的活動類型以及超過閾值時採取的操作。" @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "預設暱稱" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "删除" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "刪除機器人 {0}? 這將軟刪除資料庫記錄;稍後只有在執行 /REHASH 後才能重新使用該暱稱。" + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "删除频道" @@ -843,6 +948,10 @@ msgstr "探索" msgid "Discover the world of IRC with ObsidianIRC" msgstr "使用 ObsidianIRC 探索 IRC 的世界" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "關閉" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "关闭通知" @@ -1039,6 +1148,10 @@ msgstr "筛选频道..." msgid "Filter members…" msgstr "筛选成员…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "要傳送的第一則訊息" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "洪水設定檔 (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line(全域封禁)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway 已連線" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway 上線" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "一般" @@ -1186,6 +1307,10 @@ msgstr "第 {0} 張,共 {1} 張" msgid "Image preview" msgstr "图片预览" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "傳入" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "資訊:" @@ -1270,6 +1395,10 @@ msgstr "加入 {0}" msgid "Join {value}" msgstr "加入 {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "加入頻道" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "了解更多自訂規則 →" msgid "Learn more about profiles →" msgstr "了解更多設定檔 →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "離開頻道" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "离开频道" @@ -1411,6 +1544,14 @@ msgstr "管理您的个人资料信息和元数据" msgid "Mark as bot - usually 'on' or empty" msgstr "標記為機器人——通常為 'on' 或空白" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "將自己標記為離開" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "將自己標記為回來" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "最多使用者數" @@ -1573,6 +1714,10 @@ msgstr "网络名称" msgid "New DM" msgstr "新私訊" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "新暱稱" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "下一张图片" @@ -1617,6 +1762,10 @@ msgstr "未找到封禁例外" msgid "No bans found" msgstr "未找到封禁" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "此網路上尚未註冊任何機器人。" + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "無中央伺服器:" @@ -1672,7 +1821,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" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - 将 IRC 带入未来" msgid "Off" msgstr "关" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "離線" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "离线" @@ -1754,6 +1908,10 @@ msgstr "离线" msgid "on" msgstr "開啟" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "其中之一:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "哎呀!網路分裂!⚠️" msgid "Op" msgstr "管理员" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "開啟與使用者的私訊" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "打开频道配置设置" @@ -1828,10 +1990,23 @@ msgstr "Oper 密码" msgid "Oper Username" msgstr "Oper 使用者名稱" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "操作員操作" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "可選的當機報告以改善應用程式" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "傳出" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "輸出已截斷" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "频道所有者" @@ -1994,6 +2169,10 @@ msgstr "退出消息" msgid "Quit the server" msgstr "已退出服务器" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "執行了" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "添加表情" @@ -2024,6 +2203,10 @@ msgstr "原因" msgid "Reason (optional)" msgstr "原因(可选)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "推理" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "重新连接服务器" @@ -2037,6 +2220,11 @@ msgstr "重新整理" msgid "Register for an account" msgstr "注册账户" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "拒絕" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "宽松" @@ -2077,11 +2265,27 @@ msgstr "在伺服器上重新命名此頻道,所有使用者都會看到新名 msgid "Render markdown formatting in messages" msgstr "在訊息中渲染 Markdown 格式" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "重新開啟產生此訊息的工作流程({stepCount} 個步驟)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "回复" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "必填" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "已在聊天中回覆" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "回覆訊息已不在檢視範圍內" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "重新傳送" msgid "Rules" msgstr "規則" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "執行" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "安全" @@ -2121,6 +2329,10 @@ msgstr "已儲存" msgid "Saving..." msgstr "正在儲存..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "將聊天捲動至此回覆" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "滚动到底部" msgid "Search" msgstr "搜索" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "搜尋機器人" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "搜尋 GIF 以開始" @@ -2183,6 +2399,10 @@ msgstr "安全警告" msgid "Seek" msgstr "跳转" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "在左側選擇一個機器人以檢視其命令和管理操作。" + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "选择频道" @@ -2195,6 +2415,10 @@ msgstr "选择成员" msgid "Select or add a channel to get started." msgstr "选择或添加频道以开始使用。" +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "自助註冊" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "发送 GIF" msgid "Send a warning message to {username}" msgstr "向 {username} 发送警告消息" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "傳送動作 / 表情" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "傳送邀請" @@ -2280,6 +2508,10 @@ msgstr "服务器密码" msgid "Server-to-server communication may use unencrypted connections" msgstr "服务器之间的通信可能使用未加密的连接" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "伺服器範圍" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "设置主题…" @@ -2376,6 +2608,10 @@ msgstr "显示来自服务器受信任文件主机的媒体。不会向外部服 msgid "Signed On" msgstr "登入時間" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "斜線命令" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "軟體:" @@ -2392,10 +2628,18 @@ msgstr "依使用者數排序" msgid "SoundCloud player" msgstr "SoundCloud 播放器" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "來源" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "发起私信" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "正在啟動工作流程…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "状态消息" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "停止" @@ -2419,10 +2664,18 @@ msgstr "严格" msgid "Strict - More aggressive protection" msgstr "嚴格 – 更強的保護" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "停用" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "系统" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "文字" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "文字頻道" @@ -2447,6 +2700,14 @@ msgstr "此頻道顯示的主題,所有使用者均可查看。" msgid "The user will receive an invitation to join {channelName}." msgstr "使用者將收到加入 {channelName} 的邀請。" +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "產生此訊息的工作流程已不在狀態中" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "此命令不需要參數。" + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "此欄位為必填" @@ -2510,6 +2771,10 @@ msgstr "切换通知" msgid "Toggle search" msgstr "切换搜索" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "工具" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "主題設定時間晚於(分鐘前)" @@ -2527,6 +2792,10 @@ msgstr "話題:" msgid "Total: {0}" msgstr "總計:{0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "傳輸" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "受信任来源" @@ -2561,6 +2830,10 @@ msgstr "取消置顶私聊" msgid "Unpin this private message conversation" msgstr "取消置顶此私信对话" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "取消停用" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "上傳" @@ -2687,6 +2960,11 @@ msgstr "非常宽松" msgid "Very Strict" msgstr "非常严格" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "透過 @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "影片" @@ -2730,6 +3008,10 @@ msgstr "有發言權的使用者" msgid "Volume" msgstr "音量" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "正在等待第一個步驟…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "已被踢出频道" msgid "We don't store your IRC communications on our servers" msgstr "我們不在伺服器上儲存您的 IRC 通訊" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "欢迎来到 {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "你在做什麼?" msgid "What this means:" msgstr "这意味着:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "你正在做什麼" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "您在想什么?" @@ -2780,6 +3070,10 @@ msgstr "您在想什么?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "在目前頻道情境中對使用者悄悄話" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "寬泛 – 更廣的保護範圍" @@ -2788,6 +3082,19 @@ msgstr "寬泛 – 更廣的保護範圍" msgid "Will default to 'no reason' if left empty" msgstr "留空将默认显示\"无原因\"" +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "工作流程歷史" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "工作流程歷史({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "處理中…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/locales/zh/messages.mjs b/src/locales/zh/messages.mjs index 4d7c83a5..5a10d52d 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>风险: 敏感信息(消息、私人对话、身份验证详情)可能会暴露给网络管理员或位于 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\"],\"\"],\"chQsxg\":[\"复制格式化输出\"],\"cl/A5J\":[\"欢迎来到 \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"删除\"],\"coPLXT\":[\"我们不在服务器上存储您的 IRC 通信\"],\"crYH/6\":[\"SoundCloud 播放器\"],\"d3sis4\":[\"添加服务器\"],\"d9aN5k\":[\"将 \",[\"username\"],\" 移出频道\"],\"dEgA5A\":[\"取消\"],\"dGi1We\":[\"取消置顶此私信对话\"],\"dJVuyC\":[\"离开了 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"至\"],\"dXqxlh\":[\"<0>⚠️ 安全风险! 此连接可能容易遭受窃听或中间人攻击。\"],\"da9Q/R\":[\"已更改频道模式\"],\"dhJN3N\":[\"显示评论\"],\"dj2xTE\":[\"关闭通知\"],\"dpCzmC\":[\"洪水保护设置\"],\"e9dQpT\":[\"是否在新标签页中打开此链接?\"],\"ePK91l\":[\"编辑\"],\"eYBDuB\":[\"上传图片或提供带可选 \",[\"size\"],\" 替换的 URL\"],\"edBbee\":[\"通过 hostmask 封禁 \",[\"username\"],\"(阻止其从相同 IP/主机重新加入)\"],\"ekfzWq\":[\"用户设置\"],\"elPDWs\":[\"自定义您的 IRC 客户端体验\"],\"eu2osY\":[\"<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>⚠️ 请注意! 仅打开来自可信来源的链接。恶意链接可能危害您的安全或隐私。\"],\"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\"],\" 的连接存在以下安全问题:\"],\"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/invitation 能力)。您仍可正常聊天;此面板适用于由 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\":[\"标记为离开时的默认消息\"],\"+fRR7i\":[\"停用\"],\"+mVPqU\":[\"在消息中渲染 Markdown 格式\"],\"+vqCJH\":[\"您的账户认证用户名\"],\"+yPBXI\":[\"选择文件\"],\"+zy2Nq\":[\"类型\"],\"/09cao\":[\"链路安全级别较低(级别 \",[\"securityLevel\"],\")\"],\"/0mUpR\":[\"将自己标记为返回\"],\"/3BQ4J\":[\"频道外的用户无法发送消息\"],\"/4C8U0\":[\"全部复制\"],\"/6BzZF\":[\"切换成员列表\"],\"/AkXyp\":[\"确认?\"],\"/TNOPk\":[\"用户已离开\"],\"/XQgft\":[\"发现\"],\"/cF7Rs\":[\"音量\"],\"/dqduX\":[\"下一页\"],\"/fc3q4\":[\"所有内容\"],\"/kISDh\":[\"启用通知声音\"],\"/n04sB\":[\"踢出\"],\"/rTz0M\":[\"音频\"],\"/rfkZe\":[\"提及和消息时播放声音\"],\"/xQ19T\":[\"此网络上的机器人\"],\"0/0ZGA\":[\"频道名称掩码\"],\"0D6j7U\":[\"了解更多自定义规则 →\"],\"0XsHcR\":[\"踢出用户\"],\"0ZpE//\":[\"按用户数排序\"],\"0bEPwz\":[\"设置为离开\"],\"0dGkPt\":[\"展开频道列表\"],\"0gS7M5\":[\"显示名称\"],\"0kS+M8\":[\"示例网络\"],\"0rgoY7\":[\"仅连接您选择的服务器\"],\"0wdd7X\":[\"加入\"],\"0wkVYx\":[\"私信\"],\"111uHX\":[\"链接预览\"],\"196EG4\":[\"删除私聊\"],\"1C/fOn\":[\"机器人尚未注册任何斜杠命令。\"],\"1DSr1i\":[\"注册账户\"],\"1O/24y\":[\"切换频道列表\"],\"1QfxQT\":[\"关闭\"],\"1VPJJ2\":[\"外部链接警告\"],\"1ZC/dv\":[\"没有未读提及或消息\"],\"1pO1zi\":[\"服务器名称为必填项\"],\"1t/NnN\":[\"拒绝\"],\"1uwfzQ\":[\"查看频道主题\"],\"268g7c\":[\"输入显示名称\"],\"2F9+AZ\":[\"尚未捕获任何原始 IRC 流量。请尝试连接或发送消息。\"],\"2FOFq1\":[\"网络上的服务器管理员可能读取您的消息\"],\"2FYpfJ\":[\"更多\"],\"2HF1Y2\":[[\"inviter\"],\" 邀请了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"2I70QL\":[\"查看用户资料信息\"],\"2QYdmE\":[\"用户:\"],\"2QpEjG\":[\"已离开\"],\"2YE223\":[\"发消息到 #\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"2bimFY\":[\"使用服务器密码\"],\"2iTmdZ\":[\"本地存储:\"],\"2odkwe\":[\"严格 – 更强的保护\"],\"2uDhbA\":[\"输入要邀请的用户名\"],\"2xXP/g\":[\"加入频道\"],\"2ygf/L\":[\"← 返回\"],\"2zEgxj\":[\"搜索 GIF...\"],\"3JjdaA\":[\"运行\"],\"3NJ4MW\":[\"重新打开生成此消息的工作流(\",[\"stepCount\"],\" 个步骤)\"],\"3RdPhl\":[\"重命名频道\"],\"3THokf\":[\"有发言权的用户\"],\"3TSz9S\":[\"最小化\"],\"3et0TM\":[\"将聊天滚动到此回复\"],\"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\":[\"短消息自动切换到单行\"],\"4uKgKr\":[\"出站\"],\"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\":[\"有隐私问题?联系我们:\"],\"9q17ZR\":[[\"0\"],\" 为必填项。\"],\"9qIYMn\":[\"新昵称\"],\"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\":[\"您有未保存的更改。确定要关闭而不保存吗?\"],\"BIDT9R\":[\"机器人\"],\"BIf9fi\":[\"您的状态消息\"],\"BPm98R\":[\"未选择服务器。请先从侧边栏选择一个服务器;邀请链接按服务器分别管理。\"],\"BZz3md\":[\"您的个人网站\"],\"Bgm/H7\":[\"允许输入多行文本\"],\"BiQIl1\":[\"置顶此私信对话\"],\"BlNZZ2\":[\"点击跳转到消息\"],\"Bowq3c\":[\"只有管理员可以更改频道主题\"],\"Btozzp\":[\"此图片已过期\"],\"Bycfjm\":[\"总计:\",[\"0\"]],\"C6IBQc\":[\"复制完整 JSON\"],\"C9L9wL\":[\"数据收集\"],\"CDq4wC\":[\"管理用户\"],\"CHVRxG\":[\"发消息给 @\",[\"0\"],\"(Shift+Enter 换行)\"],\"CN9zdR\":[\"Oper 用户名和密码为必填项\"],\"CW3sYa\":[\"添加表情 \",[\"emoji\"]],\"CaAkqd\":[\"显示退出事件\"],\"CaQ1Gb\":[\"由配置定义的机器人。编辑 obbyircd.conf 并执行 /REHASH 以更改状态。\"],\"CbvaYj\":[\"按昵称封禁\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"选择频道\"],\"CsekCi\":[\"普通\"],\"D+NlUC\":[\"系统\"],\"D28t6+\":[\"已加入并退出\"],\"DB8zMK\":[\"应用\"],\"DBcWHr\":[\"自定义通知音频文件\"],\"DSHF2K\":[\"生成此消息的工作流已不在状态中\"],\"DTy9Xw\":[\"媒体预览\"],\"Dj4pSr\":[\"选择一个安全密码\"],\"Du+zn+\":[\"搜索中...\"],\"Du2T2f\":[\"未找到设置\"],\"DwsSVQ\":[\"应用筛选并刷新\"],\"E3W/zd\":[\"默认昵称\"],\"E6nRW7\":[\"复制链接\"],\"E703RG\":[\"模式:\"],\"EAeu1Z\":[\"发送邀请\"],\"EFKJQT\":[\"设置\"],\"EGPQBv\":[\"自定义洪水规则 (+f)\"],\"ELik0r\":[\"查看完整隐私政策\"],\"EPbeC2\":[\"查看或编辑频道主题\"],\"EQCDNT\":[\"输入 oper 用户名...\"],\"EUvulZ\":[\"找到 1 条匹配\\\"\",[\"searchQuery\"],\"\\\"的消息\"],\"EatZYJ\":[\"下一张图片\"],\"EdQY6l\":[\"无\"],\"EnqLYU\":[\"搜索服务器...\"],\"Eu7YKa\":[\"自助注册\"],\"F0OKMc\":[\"编辑服务器\"],\"F6Int2\":[\"启用高亮\"],\"FDoLyE\":[\"最大用户数\"],\"FUU/hZ\":[\"控制聊天中加载的外部媒体数量。\"],\"Fdp03t\":[\"开启\"],\"FfPWR0\":[\"弹窗\"],\"FjkaiT\":[\"缩小\"],\"FlqOE9\":[\"这意味着:\"],\"FolHNl\":[\"管理您的账户和身份验证\"],\"Fp2Dif\":[\"已退出服务器\"],\"G5KmCc\":[\"GZ-Line(全局 Z-Line)\"],\"GDs0lz\":[\"<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\"],\" 正在输入...\"],\"IcHxhR\":[\"离线\"],\"IgrLD/\":[\"暂停\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"回复\"],\"IoHMnl\":[\"最大值为 \",[\"0\"]],\"IvMj+0\":[\"管理员\"],\"J28zul\":[\"正在连接...\"],\"J5T9NW\":[\"用户信息\"],\"J8Y5+z\":[\"哎呀!网络分裂!⚠️\"],\"JBHkBA\":[\"已离开频道\"],\"JCwL0Q\":[\"输入原因(可选)\"],\"JFciKP\":[\"切换\"],\"JMXMCX\":[\"离开消息\"],\"JXGkhG\":[\"更改频道名称(仅限管理员)\"],\"JYiL1b\":[\"之一:\"],\"JcD7qf\":[\"更多操作\"],\"JdkA+c\":[\"隐秘 (+s)\"],\"Jmu12l\":[\"服务器频道\"],\"JvQ++s\":[\"启用 Markdown\"],\"K2jwh/\":[\"无可用 WHOIS 数据\"],\"K4vEhk\":[\"(已停用)\"],\"KAXSwC\":[\"语音权限\"],\"KDfTdX\":[\"删除消息\"],\"KKBlUU\":[\"嵌入\"],\"KM0pLb\":[\"欢迎来到本频道!\"],\"KR6W2h\":[\"取消忽略用户\"],\"KV+Bi1\":[\"仅限邀请 (+i)\"],\"KdCtwE\":[\"在重置计数器之前监控洪水活动的秒数\"],\"Kkezga\":[\"服务器密码\"],\"KsiQ/8\":[\"用户必须受邀才能加入频道\"],\"KtADxr\":[\"执行了\"],\"L+gB/D\":[\"频道信息\"],\"LC1a7n\":[\"IRC 服务器报告其服务器间链路的安全级别较低。这意味着当您的消息在网络中的 IRC 服务器之间转发时,可能未经过妥善加密,或者 SSL/TLS 证书未被正确验证。\"],\"LN3RO2\":[[\"0\"],\" 个步骤等待批准\"],\"LNfLR5\":[\"显示踢出事件\"],\"LQb0W/\":[\"显示所有事件\"],\"LU7/yA\":[\"显示用的别名,可包含空格、表情和特殊字符。真实频道名(\",[\"channelName\"],\")仍用于 IRC 命令。\"],\"LUb9O7\":[\"需要有效的服务器端口\"],\"LV4fT6\":[\"描述(可选,例如 \\\"第三季度 Beta 测试者\\\")\"],\"LYzbQ2\":[\"工具\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"隐私政策\"],\"LcuSDR\":[\"管理您的个人资料信息和元数据\"],\"LqLS9B\":[\"显示昵称变更\"],\"LsDQt2\":[\"频道设置\"],\"LtI9AS\":[\"频道所有者\"],\"LuNhhL\":[\"对此消息做出了回应\"],\"M/AZNG\":[\"头像图片 URL\"],\"M/WIer\":[\"发送消息\"],\"M45wtf\":[\"此命令不需要参数。\"],\"M8er/5\":[\"名称:\"],\"MHk+7g\":[\"上一张图片\"],\"MRorGe\":[\"私信用户\"],\"MVbSGP\":[\"时间窗口(秒)\"],\"MkpcsT\":[\"您的消息和设置存储在本地设备上\"],\"N/hDSy\":[\"标记为机器人——通常为 'on' 或空\"],\"N40H+G\":[\"全部\"],\"N7TQbE\":[\"邀请用户加入 \",[\"channelName\"]],\"NCca/o\":[\"输入默认昵称...\"],\"NQN2HS\":[\"取消停用\"],\"Nqs6B9\":[\"显示所有外部媒体。任何 URL 都可能向未知服务器发送请求。\"],\"Nt+9O7\":[\"使用 WebSocket 而非原始 TCP\"],\"NxIHzc\":[\"踢出用户\"],\"O+HhhG\":[\"在当前频道上下文中对用户进行私语\"],\"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\"]],\"RlCInP\":[\"斜杠命令\"],\"RySp6q\":[\"隐藏评论\"],\"RzfkXn\":[\"更改您在此服务器上的昵称\"],\"SPKQTd\":[\"昵称为必填项\"],\"SPVjfj\":[\"留空将默认显示\\\"无原因\\\"\"],\"SQKPvQ\":[\"邀请用户\"],\"SkZcl+\":[\"选择预定义的洪水保护配置文件。这些配置文件为不同使用场景提供均衡的保护设置。\"],\"Slr+3C\":[\"最小用户数\"],\"Spnlre\":[\"您邀请了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"T/ckN5\":[\"在查看器中打开\"],\"T91vKp\":[\"播放\"],\"TImSWn\":[\"(由 ObsidianIRC 处理)\"],\"TRDppN\":[\"Webhook\"],\"TV2Wdu\":[\"了解我们如何处理您的数据并保护您的隐私。\"],\"TgFpwD\":[\"正在应用...\"],\"TkzSFB\":[\"无更改\"],\"TtserG\":[\"输入真实姓名\"],\"Ttz9J1\":[\"输入密码...\"],\"Tz0i8g\":[\"设置\"],\"U3pytU\":[\"管理员\"],\"U7yg75\":[\"将自己标记为离开\"],\"UDb2YD\":[\"添加表情\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"您还没有创建任何邀请链接。使用上面的表单来创建您的第一个。\"],\"UGT5vp\":[\"保存设置\"],\"UV5hLB\":[\"未找到封禁\"],\"Uaj3Nd\":[\"状态消息\"],\"Ue3uny\":[\"默认(无配置文件)\"],\"UkARhe\":[\"普通 – 标准保护\"],\"Umn7Cj\":[\"暂无评论,成为第一个吧!\"],\"UqtiKk\":[[\"secondsLeft\"],\" 秒后自动关闭\"],\"UrEy4W\":[\"向用户打开私信\"],\"UtUIRh\":[[\"0\"],\" 条旧消息\"],\"UwzP+U\":[\"安全连接\"],\"V0/A4O\":[\"频道所有者\"],\"V2dwib\":[[\"0\"],\" 必须是数字。\"],\"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\":[\"取消置顶私聊\"],\"XklovM\":[\"处理中…\"],\"Xm/s+u\":[\"显示\"],\"Xp2n93\":[\"显示来自服务器受信任文件主机的媒体。不会向外部服务发送任何请求。\"],\"XvjC4F\":[\"正在保存...\"],\"Y+tK3n\":[\"要发送的第一条消息\"],\"Y/qryO\":[\"未找到与搜索条件匹配的用户\"],\"YAqRpI\":[\"账户 \",[\"account\"],\" 注册成功:\",[\"message\"]],\"YBXJ7j\":[\"入站\"],\"YEfzvP\":[\"受保护主题 (+t)\"],\"YQOn6a\":[\"收起成员列表\"],\"YRCoE9\":[\"频道操作员\"],\"YURQaF\":[\"查看资料\"],\"YdBSvr\":[\"控制媒体显示和外部内容\"],\"Yj6U3V\":[\"无中央服务器:\"],\"YjvpGx\":[\"代词\"],\"YqH4l4\":[\"无密钥\"],\"YyUPpV\":[\"账户:\"],\"Z7ZXbT\":[\"批准\"],\"ZJSWfw\":[\"断开服务器时显示的消息\"],\"ZR1dJ4\":[\"邀请\"],\"ZdWg0V\":[\"在浏览器中打开\"],\"ZhRBbl\":[\"搜索消息…\"],\"Zmcu3y\":[\"高级筛选\"],\"ZqLD8l\":[\"服务器范围\"],\"a2/8e5\":[\"主题设置时间晚于(分钟前)\"],\"aHKcKc\":[\"上一页\"],\"aJTbXX\":[\"Oper 密码\"],\"aP9gNu\":[\"输出已截断\"],\"aQryQv\":[\"模式已存在\"],\"aW9pLN\":[\"频道允许的最大用户数,留空表示无限制。\"],\"ah4fmZ\":[\"同时显示来自 YouTube、Vimeo、SoundCloud 及类似知名服务的预览。\"],\"aifXak\":[\"此频道没有媒体\"],\"ap2zBz\":[\"宽松\"],\"az8lvo\":[\"关\"],\"azXSNo\":[\"展开成员列表\"],\"azdliB\":[\"登录账户\"],\"b26wlF\":[\"她/她的\"],\"bD/+Ei\":[\"严格\"],\"bFDO8z\":[\"gateway 在线\"],\"bQ6BJn\":[\"配置详细的洪水保护规则。每条规则指定要监控的活动类型以及超过阈值时采取的操作。\"],\"bVBC/W\":[\"Gateway 已连接\"],\"beV7+y\":[\"用户将收到加入 \",[\"channelName\"],\" 的邀请。\"],\"bk84cH\":[\"离开消息\"],\"bkHdLj\":[\"添加 IRC 服务器\"],\"bmQLn5\":[\"添加规则\"],\"bv4cFj\":[\"传输\"],\"bwRvnp\":[\"操作\"],\"c8+EVZ\":[\"已验证账户\"],\"cGYUlD\":[\"未加载任何媒体预览。\"],\"cLF98o\":[\"显示评论 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"没有可用用户\"],\"cSgpoS\":[\"置顶私聊\"],\"cde3ce\":[\"发消息给 <0>\",[\"0\"],\"\"],\"chQsxg\":[\"复制格式化输出\"],\"cl/A5J\":[\"欢迎来到 \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"删除\"],\"coPLXT\":[\"我们不在服务器上存储您的 IRC 通信\"],\"crYH/6\":[\"SoundCloud 播放器\"],\"d3sis4\":[\"添加服务器\"],\"d9aN5k\":[\"将 \",[\"username\"],\" 移出频道\"],\"dEgA5A\":[\"取消\"],\"dGi1We\":[\"取消置顶此私信对话\"],\"dJVuyC\":[\"离开了 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"至\"],\"dRqrdL\":[[\"0\"],\" 必须是整数。\"],\"dXqxlh\":[\"<0>⚠️ 安全风险! 此连接可能容易遭受窃听或中间人攻击。\"],\"da9Q/R\":[\"已更改频道模式\"],\"dhJN3N\":[\"显示评论\"],\"dj2xTE\":[\"关闭通知\"],\"dnUmOX\":[\"此网络上尚未注册任何机器人。\"],\"dpCzmC\":[\"洪水保护设置\"],\"e7KzRG\":[[\"0\"],\" 个步骤\"],\"e9dQpT\":[\"是否在新标签页中打开此链接?\"],\"ePK91l\":[\"编辑\"],\"eYBDuB\":[\"上传图片或提供带可选 \",[\"size\"],\" 替换的 URL\"],\"edBbee\":[\"通过 hostmask 封禁 \",[\"username\"],\"(阻止其从相同 IP/主机重新加入)\"],\"ekfzWq\":[\"用户设置\"],\"elPDWs\":[\"自定义您的 IRC 客户端体验\"],\"eu2osY\":[\"<0>💡 建议: 仅在您信任此服务器并了解相关风险的情况下继续操作。避免通过此连接共享敏感信息或密码。\"],\"euEhbr\":[\"点击加入 \",[\"channel\"]],\"ez3vLd\":[\"启用多行输入\"],\"f0J5Ki\":[\"服务器之间的通信可能使用未加密的连接\"],\"f9BHJk\":[\"警告用户\"],\"fDOLLd\":[\"未找到频道。\"],\"fYdEvu\":[\"工作流历史(\",[\"0\"],\")\"],\"ffzDkB\":[\"匿名分析:\"],\"fq1GF9\":[\"显示用户断开服务器连接的事件\"],\"gEF57C\":[\"此服务器仅支持一种连接类型\"],\"gJuLUI\":[\"忽略列表\"],\"gNzMrk\":[\"当前头像\"],\"gjPWyO\":[\"输入昵称...\"],\"gz6UQ3\":[\"最大化\"],\"h6razj\":[\"排除频道名称掩码\"],\"hG6jnw\":[\"未设置主题\"],\"hG89Ed\":[\"图片\"],\"hYgDIe\":[\"创建\"],\"hZ6znB\":[\"端口\"],\"ha+Bz5\":[\"例如:100:1440\"],\"hctjqj\":[\"在左侧选择一个机器人以查看其命令和管理操作。\"],\"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\":[\"上传头像\"],\"jUXib7\":[\"回复消息已不在视图中\"],\"jW5Uwh\":[\"控制加载的外部媒体量。关闭 / 安全 / 可信来源 / 所有内容。\"],\"jXzms5\":[\"附件选项\"],\"jZlrte\":[\"颜色\"],\"jfC/xh\":[\"联系方式\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"加载旧消息\"],\"k3ID0F\":[\"筛选成员…\"],\"k65gsE\":[\"深入查看\"],\"k7Zgob\":[\"取消连接\"],\"kAVx5h\":[\"未找到邀请\"],\"kCLEPU\":[\"已连接到\"],\"kF5LKb\":[\"已忽略的模式:\"],\"kG2fiE\":[\"配置定义\"],\"kGeOx/\":[\"加入 \",[\"0\"]],\"kITKr8\":[\"正在加载频道模式...\"],\"kPpPsw\":[\"您是 IRC Operator\"],\"kWJmRL\":[\"您\"],\"kfcRb0\":[\"头像\"],\"kjMqSj\":[\"复制 JSON\"],\"krViRy\":[\"点击以 JSON 格式复制\"],\"ks71ra\":[\"例外\"],\"kw4lRv\":[\"频道半操作员\"],\"kxgIRq\":[\"选择或添加频道以开始使用。\"],\"ky2mw7\":[\"通过 @\",[\"0\"]],\"ky6dWe\":[\"头像预览\"],\"l+GxCv\":[\"正在加载频道...\"],\"l+IUVW\":[\"账户 \",[\"account\"],\" 验证成功:\",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[\"已重新连接 \",[\"reconnectCount\"],\" 次\"]}]],\"l1l8sj\":[[\"0\"],\" 天前\"],\"l5NhnV\":[\"#频道(可选)\"],\"l5jmzx\":[[\"0\"],\" 和 \",[\"1\"],\" 正在输入...\"],\"lCF0wC\":[\"刷新\"],\"lH+ed1\":[\"正在等待第一个步骤…\"],\"lHy8N5\":[\"正在加载更多频道...\"],\"lasgrr\":[\"已使用\"],\"lbpf14\":[\"加入 \",[\"value\"]],\"lf3MT4\":[\"要离开的频道(默认为当前频道)\"],\"lfFsZ4\":[\"频道\"],\"lkNdiH\":[\"账户名称\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"上传图片\"],\"loQxaJ\":[\"我回来了\"],\"lvfaxv\":[\"主页\"],\"m16xKo\":[\"添加\"],\"m8flAk\":[\"预览(尚未上传)\"],\"mDkV0w\":[\"正在启动工作流…\"],\"mEPxTp\":[\"<0>⚠️ 请注意! 仅打开来自可信来源的链接。恶意链接可能危害您的安全或隐私。\"],\"mHGdhG\":[\"服务器信息\"],\"mHS8lb\":[\"发消息到 #\",[\"0\"]],\"mHfd/S\":[\"您正在做什么\"],\"mMYBD9\":[\"宽泛 – 更广的保护范围\"],\"mTGsPd\":[\"频道主题\"],\"mU8j6O\":[\"禁止外部消息 (+n)\"],\"mZp8FL\":[\"自动回退到单行\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"评论 (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"用户已认证\"],\"mwtcGl\":[\"关闭评论\"],\"mzI/c+\":[\"下载\"],\"n3fGRk\":[\"由 \",[\"0\"],\" 设置\"],\"nE9jsU\":[\"宽松 – 较弱的保护\"],\"nNflMD\":[\"离开频道\"],\"nPXkBi\":[\"正在加载 WHOIS 数据...\"],\"nQnxxF\":[\"发消息到 #\",[\"0\"],\"(Shift+Enter 换行)\"],\"nWMRxa\":[\"取消置顶\"],\"nX4XLG\":[\"操作员操作\"],\"nkC032\":[\"无洪水防护配置\"],\"o69z4d\":[\"向 \",[\"username\"],\" 发送警告消息\"],\"o9ylQi\":[\"搜索 GIF 以开始\"],\"oFGkER\":[\"服务器通知\"],\"oOi11l\":[\"滚动到底部\"],\"oPYIL5\":[\"网络\"],\"oQEzQR\":[\"新私信\"],\"oXOSPE\":[\"在线\"],\"oaTtrx\":[\"搜索机器人\"],\"oal760\":[\"服务器链路可能遭受中间人攻击\"],\"oeqmmJ\":[\"受信任来源\"],\"optX0N\":[[\"0\"],\" 小时前\"],\"ovBPCi\":[\"默认\"],\"p0Z69r\":[\"模式不能为空\"],\"p1KgtK\":[\"音频加载失败\"],\"p59pEv\":[\"更多详情\"],\"p7sRI6\":[\"让其他人知道您正在输入\"],\"pBm1od\":[\"秘密频道\"],\"pNmiXx\":[\"所有服务器的默认昵称\"],\"pQBYsE\":[\"已在聊天中回复\"],\"pUUo9G\":[\"主机名:\"],\"pVGPmz\":[\"账户密码\"],\"peNE68\":[\"永久\"],\"plhHQt\":[\"无数据\"],\"pm6+q5\":[\"安全警告\"],\"pn5qSs\":[\"附加信息\"],\"q0cR4S\":[\"现在称为 **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"频道不会出现在 LIST 或 NAMES 命令中\"],\"qLpTm/\":[\"移除表情 \",[\"emoji\"]],\"qVkGWK\":[\"置顶\"],\"qXgujk\":[\"发送动作 / 表情\"],\"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\"],\" 的连接存在以下安全问题:\"],\"sGH11W\":[\"服务器\"],\"sHI1H+\":[\"现在称为 **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" 邀请您加入 \",[\"channel\"]],\"sW5OjU\":[\"必填\"],\"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\":[\"保存\"],\"thC9Rq\":[\"离开频道\"],\"tiBsJk\":[\"离开了 \",[\"channelName\"]],\"tt4/UD\":[\"退出了 (\",[\"reason\"],\")\"],\"tu2JEr\":[\"要加入的频道(#名称)\"],\"u0TcnO\":[\"昵称 {nick} 已被使用,正在用 {newNick} 重试\"],\"u0a8B4\":[\"以 IRC 运营商身份进行管理访问认证\"],\"u0rWFU\":[\"创建时间晚于(分钟前)\"],\"u72w3t\":[\"要忽略的用户和模式\"],\"u7jc2L\":[\"退出了\"],\"uAQUqI\":[\"状态\"],\"uB85T3\":[\"保存失败:\",[\"msg\"]],\"uMIUx8\":[\"删除机器人 \",[\"0\"],\"? 这将软删除数据库记录;稍后只有在执行 /REHASH 后才能重新使用该昵称。\"],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC 服务器:\"],\"ukyW4o\":[\"您的邀请链接\"],\"usSSr/\":[\"缩放级别\"],\"v7uvcf\":[\"软件:\"],\"vE8kb+\":[\"Shift+Enter 换行(Enter 发送)\"],\"vERlcd\":[\"个人资料\"],\"vK0RL8\":[\"无主题\"],\"vSJd18\":[\"视频\"],\"vXIe7J\":[\"语言\"],\"vaHYxN\":[\"真实姓名\"],\"vhjbKr\":[\"离开\"],\"w4NYox\":[[\"title\"],\" 客户端\"],\"w8xQRx\":[\"值无效\"],\"wCKe3+\":[\"工作流历史\"],\"wFjjxZ\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"未找到封禁例外\"],\"wPrGnM\":[\"频道管理员\"],\"wRkP2d\":[\"GIF\"],\"wUf2OL\":[\"推理\"],\"wbm86v\":[\"显示用户加入或离开频道的事件\"],\"wdxz7K\":[\"来源\"],\"whqZ9r\":[\"要高亮的额外词语或短语\"],\"wm7RV4\":[\"通知声音\"],\"wz/Yoq\":[\"您的消息在服务器之间转发时可能被截获\"],\"x3+y8b\":[\"通过此链接注册的人数\"],\"xCJdfg\":[\"清除\"],\"xOTzt5\":[\"刚刚\"],\"xUHRTR\":[\"连接时自动以操作员身份认证\"],\"xWHwwQ\":[\"封禁\"],\"xYilR2\":[\"媒体\"],\"xbi8D6\":[\"此服务器不支持邀请链接(未声明 <0>obby.world/invitation 能力)。您仍可正常聊天;此面板适用于由 obbyircd 提供支持的网络。\"],\"xceQrO\":[\"仅支持安全的 WebSocket 连接\"],\"xdtXa+\":[\"频道名称\"],\"xeiujy\":[\"文本\"],\"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\"]],\"zPBDzU\":[\"取消工作流\"],\"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..b2658467 100644 --- a/src/locales/zh/messages.po +++ b/src/locales/zh/messages.po @@ -22,6 +22,14 @@ msgstr "ObsidianIRC - 将 IRC 带入未来" msgid "— open in viewer" msgstr "— 在查看器中打开" +#: src/components/ui/SlashParamHint.tsx +msgid "(handled by ObsidianIRC)" +msgstr "(由 ObsidianIRC 处理)" + +#: src/components/ui/BotsModal.tsx +msgid "(suspended)" +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); @@ -41,16 +49,41 @@ msgstr "{0, plural, other {显示另外 {1} 个项目}}" msgid "{0} and {1} are typing..." msgstr "{0} 和 {1} 正在输入..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} is required." +msgstr "{0} 为必填项。" + #. placeholder {0}: typingUsers[0].username #: src/components/layout/ChatArea.tsx msgid "{0} is typing..." msgstr "{0} 正在输入..." +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a number." +msgstr "{0} 必须是数字。" + +#. placeholder {0}: o.name +#: src/components/ui/SlashCommandParamModal.tsx +msgid "{0} must be a whole number." +msgstr "{0} 必须是整数。" + #. placeholder {0}: filteredMessages.length - displayedMessages.length #: src/components/layout/ChannelMessageList.tsx msgid "{0} older messages" msgstr "{0} 条旧消息" +#. placeholder {0}: countableSteps(w.steps) +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "{0} step(s)" +msgstr "{0} 个步骤" + +#. placeholder {0}: pendingApprovals.length +#: src/components/ui/BotToolsCard.tsx +msgid "{0} step(s) awaiting approval" +msgstr "{0} 个步骤等待批准" + #. placeholder {0}: typingUsers[0].username #. placeholder {1}: typingUsers[1].username #. placeholder {2}: typingUsers[2].username @@ -252,6 +285,10 @@ msgstr "高级筛选" msgid "Album" msgstr "相册" +#: src/components/ui/BotsModal.tsx +msgid "All" +msgstr "全部" + #: src/components/ui/UserSettings.tsx msgid "All Content" msgstr "所有内容" @@ -297,6 +334,12 @@ msgstr "应用筛选并刷新" msgid "Applying..." msgstr "正在应用..." +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Approve" +msgstr "批准" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "are now known as **{newNick}**" @@ -330,6 +373,10 @@ msgstr "已验证账户" msgid "Auto Fallback to Single Line" msgstr "自动回退到单行" +#: src/components/ui/BotToolsCard.tsx +msgid "Auto-dismiss in {secondsLeft}s" +msgstr "{secondsLeft} 秒后自动关闭" + #: src/lib/settings/definitions/allSettings.ts msgid "Automatically authenticate as operator on connect" msgstr "连接时自动以操作员身份认证" @@ -359,6 +406,10 @@ msgstr "离开" msgid "Away from keyboard" msgstr "暂时离开" +#: src/lib/clientCommands.ts +msgid "Away message" +msgstr "离开消息" + #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Away Message" @@ -368,6 +419,7 @@ msgstr "离开消息" msgid "Away:" msgstr "离开:" +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/UserSettings.tsx msgid "Back" @@ -413,11 +465,28 @@ msgstr "封禁" #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts msgid "Bot" msgstr "机器人" +#: src/components/ui/BotsModal.tsx +msgid "Bot hasn't registered any slash commands yet." +msgstr "机器人尚未注册任何斜杠命令。" + +#: src/components/layout/ChatHeader.tsx +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots" +msgstr "机器人" + +#: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx +msgid "Bots on this network" +msgstr "此网络上的机器人" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Browse all channels on the server" msgstr "浏览服务器上的所有频道" @@ -430,6 +499,7 @@ msgstr "浏览服务器上的所有频道" #: src/components/ui/ImagePreviewModal.tsx #: src/components/ui/InviteUserModal.tsx #: src/components/ui/ModerationModal.tsx +#: src/components/ui/SlashCommandParamModal.tsx #: src/components/ui/UserSettings.tsx #: src/components/ui/UserSettings.tsx msgid "Cancel" @@ -443,10 +513,18 @@ msgstr "取消连接" msgid "Cancel reply" msgstr "取消回复" +#: src/components/ui/BotToolsCard.tsx +msgid "Cancel workflow" +msgstr "取消工作流" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Change the channel name (operators only)" msgstr "更改频道名称(仅限管理员)" +#: src/lib/clientCommands.ts +msgid "Change your nickname on this server" +msgstr "更改您在此服务器上的昵称" + #: src/components/message/EventMessage.tsx msgid "Changed channel modes" msgstr "已更改频道模式" @@ -459,6 +537,7 @@ msgstr "已更改昵称" msgid "changed the topic to: {topic}" msgstr "将话题更改为:{topic}" +#: src/components/ui/BotsModal.tsx #: src/components/ui/QuickActions.tsx msgid "Channel" msgstr "频道" @@ -519,6 +598,14 @@ msgstr "频道所有者" msgid "Channel Settings" msgstr "频道设置" +#: src/lib/clientCommands.ts +msgid "Channel to join (#name)" +msgstr "要加入的频道(#名称)" + +#: src/lib/clientCommands.ts +msgid "Channel to leave (defaults to current)" +msgstr "要离开的频道(默认为当前频道)" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Channel Topic" msgstr "频道主题" @@ -531,6 +618,7 @@ msgstr "频道不会出现在 LIST 或 NAMES 命令中" msgid "channel-name" msgstr "频道名称" +#: src/components/ui/BotsModal.tsx #: src/components/ui/UserProfileModal.tsx msgid "Channels" msgstr "频道" @@ -606,6 +694,9 @@ msgstr "客户端限制 (+l)" #: src/components/layout/ChannelList.tsx #: src/components/message/ServerNoticesPopup.tsx #: src/components/message/ServerNoticesPopup.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/ChannelSettingsModal.tsx #: src/components/ui/MediaViewerModal.tsx @@ -656,6 +747,14 @@ msgstr "评论" msgid "Comments ({commentCount})" msgstr "评论 ({commentCount})" +#: src/components/ui/BotsModal.tsx +msgid "config-defined" +msgstr "配置定义" + +#: src/components/ui/BotsModal.tsx +msgid "Config-defined bot. Edit obbyircd.conf and /REHASH to change state." +msgstr "由配置定义的机器人。编辑 obbyircd.conf 并执行 /REHASH 以更改状态。" + #: src/components/ui/FloodSettingsModal.tsx msgid "Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded." msgstr "配置详细的洪水保护规则。每条规则指定要监控的活动类型以及超过阈值时采取的操作。" @@ -802,10 +901,16 @@ msgid "Default Nickname" msgstr "默认昵称" #: src/components/mobile/MessageBottomSheet.tsx +#: src/components/ui/BotsModal.tsx #: src/components/ui/InvitationsPanel.tsx msgid "Delete" msgstr "删除" +#. placeholder {0}: bot.nick +#: src/components/ui/BotsModal.tsx +msgid "Delete bot {0}? This soft-deletes the database row; reuse the nick later only after a /REHASH." +msgstr "删除机器人 {0}? 这将软删除数据库记录;稍后只有在执行 /REHASH 后才能重新使用该昵称。" + #: src/components/layout/ChannelList.tsx msgid "Delete Channel" msgstr "删除频道" @@ -843,6 +948,10 @@ msgstr "发现" msgid "Discover the world of IRC with ObsidianIRC" msgstr "使用 ObsidianIRC 探索 IRC 的世界" +#: src/components/ui/BotToolsCard.tsx +msgid "Dismiss" +msgstr "关闭" + #: src/components/ui/GlobalNotifications.tsx msgid "Dismiss notification" msgstr "关闭通知" @@ -1039,6 +1148,10 @@ msgstr "筛选频道..." msgid "Filter members…" msgstr "筛选成员…" +#: src/lib/clientCommands.ts +msgid "First message to send" +msgstr "要发送的第一条消息" + #: src/components/ui/FloodSettingsModal.tsx msgid "Flood Profile (+F)" msgstr "洪水配置文件 (+F)" @@ -1069,6 +1182,14 @@ msgstr "G-Line" msgid "G-Line (Global Ban)" msgstr "G-Line(全局封禁)" +#: src/components/ui/BotsModal.tsx +msgid "Gateway connected" +msgstr "Gateway 已连接" + +#: src/components/ui/BotsModal.tsx +msgid "gateway online" +msgstr "gateway 在线" + #: src/components/ui/ChannelSettingsModal.tsx msgid "General" msgstr "常规" @@ -1186,6 +1307,10 @@ msgstr "第 {0} 张,共 {1} 张" msgid "Image preview" msgstr "图片预览" +#: src/components/ui/BotToolsCard.tsx +msgid "IN" +msgstr "入站" + #: src/components/message/JsonLogMessage.tsx msgid "Info:" msgstr "信息:" @@ -1270,6 +1395,10 @@ msgstr "加入 {0}" msgid "Join {value}" msgstr "加入 {value}" +#: src/lib/clientCommands.ts +msgid "Join a channel" +msgstr "加入频道" + #: src/store/handlers/users.ts #: src/store/handlers/users.ts msgid "joined {channelName}" @@ -1317,6 +1446,10 @@ msgstr "了解更多自定义规则 →" msgid "Learn more about profiles →" msgstr "了解更多配置文件 →" +#: src/lib/clientCommands.ts +msgid "Leave a channel" +msgstr "离开频道" + #: src/components/layout/ChannelList.tsx msgid "Leave channel" msgstr "离开频道" @@ -1411,6 +1544,14 @@ msgstr "管理您的个人资料信息和元数据" msgid "Mark as bot - usually 'on' or empty" msgstr "标记为机器人——通常为 'on' 或空" +#: src/lib/clientCommands.ts +msgid "Mark yourself as away" +msgstr "将自己标记为离开" + +#: src/lib/clientCommands.ts +msgid "Mark yourself as back" +msgstr "将自己标记为返回" + #: src/components/ui/ChannelListModal.tsx msgid "Max Users" msgstr "最大用户数" @@ -1573,6 +1714,10 @@ msgstr "网络名称" msgid "New DM" msgstr "新私信" +#: src/lib/clientCommands.ts +msgid "New nickname" +msgstr "新昵称" + #: src/components/ui/MediaViewerModal.tsx msgid "Next image" msgstr "下一张图片" @@ -1617,6 +1762,10 @@ msgstr "未找到封禁例外" msgid "No bans found" msgstr "未找到封禁" +#: src/components/ui/BotsModal.tsx +msgid "No bots registered on this network yet." +msgstr "此网络上尚未注册任何机器人。" + #: src/components/ui/UserSettings.tsx msgid "No Central Server:" msgstr "无中央服务器:" @@ -1672,7 +1821,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" @@ -1744,9 +1893,14 @@ msgstr "ObsidianIRC - 将 IRC 带入未来" msgid "Off" msgstr "关" +#: src/components/ui/BotsModal.tsx +msgid "offline" +msgstr "离线" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx +#: src/components/ui/BotsModal.tsx msgid "Offline" msgstr "离线" @@ -1754,6 +1908,10 @@ msgstr "离线" msgid "on" msgstr "开启" +#: src/components/ui/SlashParamHint.tsx +msgid "one of:" +msgstr "之一:" + #: src/components/layout/ChannelList.tsx #: src/components/layout/ChannelList.tsx #: src/components/layout/ChatHeader.tsx @@ -1784,6 +1942,10 @@ msgstr "哎呀!网络分裂!⚠️" msgid "Op" msgstr "管理员" +#: src/lib/clientCommands.ts +msgid "Open a private message to a user" +msgstr "向用户打开私信" + #: src/components/ui/QuickActions/uiActionConfig.tsx msgid "Open channel configuration settings" msgstr "打开频道配置设置" @@ -1828,10 +1990,23 @@ msgstr "Oper 密码" msgid "Oper Username" msgstr "Oper 用户名" +#: src/components/ui/BotsModal.tsx +msgid "Operator actions" +msgstr "操作员操作" + #: src/components/ui/UserSettings.tsx msgid "Optional crash reports to improve the app" msgstr "可选的崩溃报告以改善应用" +#: src/components/ui/BotToolsCard.tsx +msgid "OUT" +msgstr "出站" + +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "output truncated" +msgstr "输出已截断" + #: src/components/ui/UserProfileModal.tsx msgid "Owner" msgstr "频道所有者" @@ -1994,6 +2169,10 @@ msgstr "退出消息" msgid "Quit the server" msgstr "已退出服务器" +#: src/components/message/BotInvocationChip.tsx +msgid "ran" +msgstr "执行了" + #: src/components/mobile/MessageBottomSheet.tsx msgid "React" msgstr "添加表情" @@ -2024,6 +2203,10 @@ msgstr "原因" msgid "Reason (optional)" msgstr "原因(可选)" +#: src/components/ui/BotToolsCard.tsx +msgid "Reasoning" +msgstr "推理" + #: src/components/layout/ServerList.tsx msgid "Reconnect to server" msgstr "重新连接服务器" @@ -2037,6 +2220,11 @@ msgstr "刷新" msgid "Register for an account" msgstr "注册账户" +#: src/components/ui/BotToolsCard.tsx +#: src/components/ui/BotToolsCard.tsx +msgid "Reject" +msgstr "拒绝" + #: src/components/ui/ChannelSettingsModal.tsx msgid "Relaxed" msgstr "宽松" @@ -2077,11 +2265,27 @@ msgstr "在服务器上重命名此频道,所有用户都会看到新名称。 msgid "Render markdown formatting in messages" msgstr "在消息中渲染 Markdown 格式" +#: src/components/message/BotToolsMessagePill.tsx +msgid "Reopen the workflow that produced this message ({stepCount} steps)" +msgstr "重新打开生成此消息的工作流({stepCount} 个步骤)" + #: src/components/message/MessageActions.tsx #: src/components/mobile/MessageBottomSheet.tsx msgid "Reply" msgstr "回复" +#: src/components/ui/SlashParamHint.tsx +msgid "required" +msgstr "必填" + +#: src/components/ui/BotToolsCard.tsx +msgid "Responded in chat" +msgstr "已在聊天中回复" + +#: src/components/ui/BotToolsCard.tsx +msgid "Response message is no longer in view" +msgstr "回复消息已不在视图中" + #: src/components/message/MessageStatusIndicator.tsx #: src/components/message/MessageStatusIndicator.tsx msgid "Retry sending" @@ -2091,6 +2295,10 @@ msgstr "重新发送" msgid "Rules" msgstr "规则" +#: src/components/ui/SlashCommandParamModal.tsx +msgid "Run" +msgstr "运行" + #: src/components/ui/UserSettings.tsx msgid "Safe" msgstr "安全" @@ -2121,6 +2329,10 @@ msgstr "已保存" msgid "Saving..." msgstr "正在保存..." +#: src/components/ui/BotToolsCard.tsx +msgid "Scroll chat to this response" +msgstr "将聊天滚动到此回复" + #: src/components/ui/LinkSecurityWarningModal.tsx #: src/components/ui/ScrollToBottomButton.tsx msgid "Scroll to bottom" @@ -2134,6 +2346,10 @@ msgstr "滚动到底部" msgid "Search" msgstr "搜索" +#: src/components/ui/BotsModal.tsx +msgid "Search bots" +msgstr "搜索机器人" + #: src/components/ui/GifSelector.tsx msgid "Search for GIFs to get started" msgstr "搜索 GIF 以开始" @@ -2183,6 +2399,10 @@ msgstr "安全警告" msgid "Seek" msgstr "跳转" +#: src/components/ui/BotsModal.tsx +msgid "Select a bot on the left to see its commands and management actions." +msgstr "在左侧选择一个机器人以查看其命令和管理操作。" + #: src/components/layout/ChatHeader.tsx msgid "Select a channel" msgstr "选择频道" @@ -2195,6 +2415,10 @@ msgstr "选择成员" msgid "Select or add a channel to get started." msgstr "选择或添加频道以开始使用。" +#: src/components/ui/BotsModal.tsx +msgid "self-registered" +msgstr "自助注册" + #: src/components/layout/ChatArea.tsx #: src/components/ui/GifSelector.tsx msgid "Send a GIF" @@ -2204,6 +2428,10 @@ msgstr "发送 GIF" msgid "Send a warning message to {username}" msgstr "向 {username} 发送警告消息" +#: src/lib/clientCommands.ts +msgid "Send an action / emote" +msgstr "发送动作 / 表情" + #: src/components/ui/InviteUserModal.tsx msgid "Send Invite" msgstr "发送邀请" @@ -2280,6 +2508,10 @@ msgstr "服务器密码" msgid "Server-to-server communication may use unencrypted connections" msgstr "服务器之间的通信可能使用未加密的连接" +#: src/components/ui/BotsModal.tsx +msgid "Server-wide" +msgstr "服务器范围" + #: src/components/ui/TopicModal.tsx msgid "Set a topic…" msgstr "设置主题…" @@ -2376,6 +2608,10 @@ msgstr "显示来自服务器受信任文件主机的媒体。不会向外部服 msgid "Signed On" msgstr "登录时间" +#: src/components/ui/BotsModal.tsx +msgid "Slash commands" +msgstr "斜杠命令" + #: src/components/message/JsonLogMessage.tsx msgid "Software:" msgstr "软件:" @@ -2392,10 +2628,18 @@ msgstr "按用户数排序" msgid "SoundCloud player" msgstr "SoundCloud 播放器" +#: src/components/ui/BotsModal.tsx +msgid "Source" +msgstr "来源" + #: src/components/ui/AddPrivateChatModal.tsx msgid "Start Private Message" msgstr "发起私信" +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Starting workflow…" +msgstr "正在启动工作流…" + #: src/components/ui/UserProfileModal.tsx #: src/components/ui/UserSettings.tsx #: src/lib/settings/definitions/allSettings.ts @@ -2407,6 +2651,7 @@ msgid "Status Messages" msgstr "状态消息" #: src/components/message/MediaPreview.tsx +#: src/components/ui/BotToolsCard.tsx #: src/components/ui/MiniMediaPlayer.tsx msgid "Stop" msgstr "停止" @@ -2419,10 +2664,18 @@ msgstr "严格" msgid "Strict - More aggressive protection" msgstr "严格 – 更强的保护" +#: src/components/ui/BotsModal.tsx +msgid "Suspend" +msgstr "停用" + #: src/components/message/ActionMessage.tsx msgid "System" msgstr "系统" +#: src/components/ui/BotToolsCard.tsx +msgid "Text" +msgstr "文本" + #: src/components/layout/ChannelList.tsx msgid "Text Channels" msgstr "文字频道" @@ -2447,6 +2700,14 @@ msgstr "此频道显示的主题,所有用户均可查看。" msgid "The user will receive an invitation to join {channelName}." msgstr "用户将收到加入 {channelName} 的邀请。" +#: src/components/message/BotToolsMessagePill.tsx +msgid "The workflow that produced this message is no longer in state" +msgstr "生成此消息的工作流已不在状态中" + +#: src/components/ui/SlashCommandParamModal.tsx +msgid "This command takes no parameters." +msgstr "此命令不需要参数。" + #: src/lib/settings/registry.ts msgid "This field is required" msgstr "此字段为必填项" @@ -2510,6 +2771,10 @@ msgstr "切换通知" msgid "Toggle search" msgstr "切换搜索" +#: src/components/ui/BotToolsCard.tsx +msgid "Tool" +msgstr "工具" + #: src/components/ui/ChannelListModal.tsx msgid "Topic Set After (min ago)" msgstr "主题设置时间晚于(分钟前)" @@ -2527,6 +2792,10 @@ msgstr "话题:" msgid "Total: {0}" msgstr "总计:{0}" +#: src/components/ui/BotsModal.tsx +msgid "Transport" +msgstr "传输" + #: src/components/ui/UserSettings.tsx msgid "Trusted Sources" msgstr "受信任来源" @@ -2561,6 +2830,10 @@ msgstr "取消置顶私聊" msgid "Unpin this private message conversation" msgstr "取消置顶此私信对话" +#: src/components/ui/BotsModal.tsx +msgid "Unsuspend" +msgstr "取消停用" + #: src/components/ui/ImagePreviewModal.tsx msgid "Upload" msgstr "上传" @@ -2687,6 +2960,11 @@ msgstr "非常宽松" msgid "Very Strict" msgstr "非常严格" +#. placeholder {0}: entry.botNick +#: src/components/ui/SlashParamHint.tsx +msgid "via @{0}" +msgstr "通过 @{0}" + #: src/components/ui/MediaCommentsSidebar.tsx msgid "Video" msgstr "视频" @@ -2730,6 +3008,10 @@ msgstr "有发言权的用户" msgid "Volume" msgstr "音量" +#: src/components/ui/BotToolsCard.tsx +msgid "Waiting for first step…" +msgstr "正在等待第一个步骤…" + #: src/components/ui/ModerationModal.tsx #: src/components/ui/UserContextMenu.tsx msgid "Warn User" @@ -2751,6 +3033,10 @@ msgstr "已被踢出频道" msgid "We don't store your IRC communications on our servers" msgstr "我们不在服务器上存储您的 IRC 通信" +#: src/components/ui/BotsModal.tsx +msgid "Webhook" +msgstr "Webhook" + #: src/components/ui/HomeScreen.tsx msgid "Welcome to {__DEFAULT_IRC_SERVER_NAME__}!" msgstr "欢迎来到 {__DEFAULT_IRC_SERVER_NAME__}!" @@ -2772,6 +3058,10 @@ msgstr "你在做什么?" msgid "What this means:" msgstr "这意味着:" +#: src/lib/clientCommands.ts +msgid "What you're doing" +msgstr "您正在做什么" + #: src/components/ui/UserSettings.tsx msgid "What's on your mind?" msgstr "您在想什么?" @@ -2780,6 +3070,10 @@ msgstr "您在想什么?" msgid "WHISPER" msgstr "WHISPER" +#: src/lib/clientCommands.ts +msgid "Whisper to a user in the current channel context" +msgstr "在当前频道上下文中对用户进行私语" + #: src/components/ui/FloodSettingsModal.tsx msgid "Wide - Broader protection scope" msgstr "宽泛 – 更广的保护范围" @@ -2788,6 +3082,19 @@ msgstr "宽泛 – 更广的保护范围" msgid "Will default to 'no reason' if left empty" msgstr "留空将默认显示\"无原因\"" +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history" +msgstr "工作流历史" + +#. placeholder {0}: workflows.length +#: src/components/ui/BotToolsHistoryButton.tsx +msgid "Workflow history ({0})" +msgstr "工作流历史({0})" + +#: src/components/message/BotToolsPlaceholderBody.tsx +msgid "Working…" +msgstr "处理中…" + #: src/components/message/CollapsedEventMessage.tsx #: src/components/message/EventMessage.tsx msgid "You" diff --git a/src/store/handlers/botTools.ts b/src/store/handlers/botTools.ts new file mode 100644 index 00000000..ef5b0361 --- /dev/null +++ b/src/store/handlers/botTools.ts @@ -0,0 +1,361 @@ +import type { StoreApi } from "zustand"; +import { + type AiStepMessage, + type AiWorkflowMessage, + BOT_TOOLS_TAG, + decodeBotToolsValue, +} from "../../lib/botTools"; +import ircClient from "../../lib/ircClient"; +import { extractMentions } from "../../lib/notifications"; +import type { Message } from "../../types"; +import { resolveReplyMessage } from "../helpers"; +import type { AiStep, AiWorkflow, AppState } from "../index"; + +function nowMs(): number { + return Date.now(); +} + +function emptyWorkflow( + serverId: string, + channel: string, + senderNick: string, + m: AiWorkflowMessage, + historical: boolean, +): AiWorkflow { + return { + id: m.id, + serverId, + channel, + senderNick, + name: m.name, + state: m.state, + trigger: m.trigger, + prompt: m.prompt, + cancelledBy: m["cancelled-by"], + startedAt: nowMs(), + updatedAt: nowMs(), + steps: [], + collapsed: true, + dismissed: false, + historical, + }; +} + +function applyWorkflowUpdate( + prev: AiWorkflow | undefined, + serverId: string, + channel: string, + senderNick: string, + m: AiWorkflowMessage, + historical: boolean, +): AiWorkflow { + if (!prev) return emptyWorkflow(serverId, channel, senderNick, m, historical); + // Stickiness: if the workflow was first seen historical, keep it that + // way even if later events (rare) arrive live. The converse -- live + // workflow gets later replayed events -- can't happen in practice. + return { + ...prev, + state: m.state, + name: m.name ?? prev.name, + trigger: m.trigger ?? prev.trigger, + prompt: m.prompt ?? prev.prompt, + cancelledBy: m["cancelled-by"] ?? prev.cancelledBy, + updatedAt: nowMs(), + }; +} + +function applyStepUpdate( + prev: AiWorkflow | undefined, + m: AiStepMessage, +): { workflow: AiWorkflow; created: boolean } | undefined { + if (!prev) return undefined; + const idx = prev.steps.findIndex((s) => s.sid === m.sid); + const baseStep: AiStep = + idx === -1 + ? { + sid: m.sid, + type: m.type, + state: m.state, + tool: m.tool, + label: m.label, + content: m.content, + truncated: m.truncated, + startedAt: nowMs(), + updatedAt: nowMs(), + } + : { + ...prev.steps[idx], + type: m.type, + state: m.state, + tool: m.tool ?? prev.steps[idx].tool, + label: m.label ?? prev.steps[idx].label, + // For string content during a content-stream the spec says + // concatenate fragments in order; for object content (tool-call + // args) the latest update wins. + content: + typeof m.content === "string" && + typeof prev.steps[idx].content === "string" + ? (prev.steps[idx].content as string) + m.content + : m.content !== undefined + ? m.content + : prev.steps[idx].content, + truncated: m.truncated ?? prev.steps[idx].truncated, + updatedAt: nowMs(), + }; + const steps = + idx === -1 + ? [...prev.steps, baseStep] + : prev.steps.map((s, i) => (i === idx ? baseStep : s)); + return { + workflow: { ...prev, steps, updatedAt: nowMs() }, + created: idx === -1, + }; +} + +export function registerBotToolsHandlers(store: StoreApi): void { + // Decide whether an event arrived inside a CHATHISTORY batch by + // looking at the @batch tag and resolving its type against the + // store's active-batch map. Returns true for replayed events, so + // the floating tray can stay quiet on channel join. + const isReplayed = ( + serverId: string, + mtags: Record | undefined, + ): boolean => { + const batchId = mtags?.batch; + if (!batchId) return false; + const batch = store.getState().activeBatches[serverId]?.[batchId]; + return batch?.type === "chathistory"; + }; + + const handleTaggedMessage = ({ + serverId, + mtags, + sender, + target, + fromPrivmsg, + body, + }: { + serverId: string; + mtags?: Record; + sender: string; + target: string; + fromPrivmsg: boolean; + body?: string; + }): void => { + const raw = mtags?.[BOT_TOOLS_TAG]; + if (!raw) return; + const msg = decodeBotToolsValue(raw); + if (!msg) return; + const historical = isReplayed(serverId, mtags); + + if (msg.msg === "workflow") { + // The bot's final answer is a PRIVMSG carrying the workflow tag. + // Stash its msgid so the workflow card can deep-link back to the + // chat message that closed it ("Responded in chat" footer). + const finalMsgid = fromPrivmsg ? mtags?.msgid : undefined; + store.setState((state) => { + const server = state.aiWorkflows[serverId] ?? {}; + const merged = applyWorkflowUpdate( + server[msg.id], + serverId, + target, + sender, + msg, + historical, + ); + if (finalMsgid && !merged.finalMsgid) { + merged.finalMsgid = finalMsgid; + } + const aiWorkflows = { + ...state.aiWorkflows, + [serverId]: { ...server, [msg.id]: merged }, + }; + + // In-chat placeholder Message owned by botTools. We create one + // on live workflow start, then morph it in place when the + // final PRIVMSG arrives so the row reads as the bot's answer + // (with the workflow pill alongside) without ever jumping + // position in the chat list. + const channel = target.startsWith("#") + ? state.servers + .find((s) => s.id === serverId) + ?.channels.find( + (c) => c.name.toLowerCase() === target.toLowerCase(), + ) + : undefined; + const isPmTarget = !target.startsWith("#"); + + if (!channel && !isPmTarget) { + return { aiWorkflows }; + } + if (historical) { + // Replayed workflows don't get a placeholder -- the original + // PRIVMSG already exists in chathistory and carries the pill. + return { aiWorkflows }; + } + + const channelKey = channel && `${serverId}-${channel.id}`; + if (!channelKey) return { aiWorkflows }; + const placeholderId = `bot-wf-${serverId}-${msg.id}`; + const existing = state.messages[channelKey] ?? []; + const idx = existing.findIndex((m) => m.id === placeholderId); + const isTerminal = + msg.state === "complete" || + msg.state === "failed" || + msg.state === "cancelled"; + + // PRIVMSG carrying the workflow tag -> morph the placeholder + // into the real message. Add the msgid to processedMessageIds + // so the generic CHANMSG handler (which runs after this one) + // skips appending its own duplicate row. + if (fromPrivmsg) { + const processedMessageIds = mtags?.msgid + ? new Set([...state.processedMessageIds, mtags.msgid]) + : state.processedMessageIds; + if (idx >= 0) { + // Resolve reply + mentions the same way the generic CHANMSG + // handler would, so the morphed row reads identically to a + // plain bot reply (quote block, ping highlight, etc.) and + // doesn't lose context just because we're reusing the + // placeholder slot. + const replyMessage = resolveReplyMessage( + mtags, + serverId, + channel?.id ?? "", + existing, + ); + const currentUser = ircClient.getCurrentUser(serverId); + const globalSettings = state.globalSettings; + const isOwnMessage = + sender.toLowerCase() === currentUser?.username?.toLowerCase(); + const mentions = + !isOwnMessage && body + ? extractMentions(body, currentUser, globalSettings) + : []; + const morphed: Message = { + ...existing[idx], + msgid: mtags?.msgid, + tags: mtags, + content: body ?? existing[idx].content, + timestamp: new Date(), + userId: sender, + replyMessage, + mentioned: mentions, + botToolsPending: false, + }; + const updated = existing.slice(); + updated[idx] = morphed; + return { + aiWorkflows, + messages: { ...state.messages, [channelKey]: updated }, + processedMessageIds, + }; + } + return { aiWorkflows, processedMessageIds }; + } + + // Terminal-state TAGMSG and the placeholder is still pending + // (i.e. the bot didn't tag its final PRIVMSG so the morph path + // above never ran). Remove the empty placeholder so the bot's + // untagged reply -- which the CHANMSG handler will append as a + // normal row, complete with reply quote -- isn't shadowed by a + // stuck "Thinking…" spinner. + if (isTerminal && idx >= 0 && existing[idx].botToolsPending) { + const updated = existing.slice(); + updated.splice(idx, 1); + return { + aiWorkflows, + messages: { ...state.messages, [channelKey]: updated }, + }; + } + + // TAGMSG workflow event (start / state update). Create the + // placeholder on first sight, otherwise leave existing + // state alone (steps update aiWorkflows, the placeholder + // re-renders reactively against it). + if (idx === -1 && msg.state === "start") { + const placeholder: Message = { + id: placeholderId, + type: "message", + content: "", + timestamp: new Date(), + userId: sender, + channelId: channel?.id ?? "", + serverId, + reactions: [], + replyMessage: null, + mentioned: [], + botToolsWorkflowId: msg.id, + botToolsPending: true, + }; + return { + aiWorkflows, + messages: { + ...state.messages, + [channelKey]: [...existing, placeholder], + }, + }; + } + return { aiWorkflows }; + }); + return; + } + + if (msg.msg === "step") { + store.setState((state) => { + const server = state.aiWorkflows[serverId] ?? {}; + const prev = server[msg.wid]; + const result = applyStepUpdate(prev, msg); + if (!result) return state; + return { + aiWorkflows: { + ...state.aiWorkflows, + [serverId]: { ...server, [msg.wid]: result.workflow }, + }, + }; + }); + return; + } + + // "action" messages echoed back from a bot are rare; ignore here. + }; + + ircClient.on("TAGMSG", ({ serverId, mtags, sender, channelName }) => { + handleTaggedMessage({ + serverId, + mtags, + sender, + target: channelName, + fromPrivmsg: false, + }); + }); + + // PRIVMSG carrying the bot-tools tag is the bot's final response. We do + // NOT suppress it from chat -- the user wants to see the answer -- but + // we mirror its workflow-state update into the workflow record so the + // card reflects the same lifecycle, and we record the message's msgid + // so the card can deep-link to it. + ircClient.on( + "CHANMSG", + ({ serverId, mtags, sender, channelName, message }) => { + handleTaggedMessage({ + serverId, + mtags, + sender, + target: channelName, + fromPrivmsg: true, + body: message, + }); + }, + ); + ircClient.on("USERMSG", ({ serverId, mtags, sender, target, message }) => { + handleTaggedMessage({ + serverId, + mtags, + sender, + target, + fromPrivmsg: true, + body: message, + }); + }); +} diff --git a/src/store/handlers/index.ts b/src/store/handlers/index.ts index 91b7e0a6..53ea2069 100644 --- a/src/store/handlers/index.ts +++ b/src/store/handlers/index.ts @@ -2,12 +2,14 @@ import type { StoreApi } from "zustand"; import type { AppState } from "../index"; import { registerAuthHandlers } from "./auth"; import { registerBatchHandlers } from "./batches"; +import { registerBotToolsHandlers } from "./botTools"; import { registerChannelHandlers } from "./channels"; import { registerConnectionHandlers } from "./connection"; import { registerInvitelinkHandlers } from "./invitelink"; import { registerMessageHandlers } from "./messages"; import { registerMetadataHandlers } from "./metadata"; import { registerNamedModesHandlers } from "./named-modes"; +import { registerPushBotHandlers } from "./pushbot"; import { registerReadMarkerHandlers } from "./readMarker"; import { registerTicTacToeHandlers } from "./tictactoe"; import { registerUserHandlers } from "./users"; @@ -15,6 +17,11 @@ import { registerWhoisHandlers } from "./whois"; export function registerAllHandlers(store: StoreApi): void { registerConnectionHandlers(store); + // botTools fires before message handlers so the workflow-PRIVMSG can + // morph an existing placeholder Message in place (and pre-add its + // msgid to processedMessageIds) before the generic CHANMSG handler + // would otherwise append a duplicate row. + registerBotToolsHandlers(store); registerMessageHandlers(store); registerUserHandlers(store); registerChannelHandlers(store); @@ -26,4 +33,5 @@ export function registerAllHandlers(store: StoreApi): void { registerNamedModesHandlers(store); registerReadMarkerHandlers(store); registerTicTacToeHandlers(store); + registerPushBotHandlers(store); } diff --git a/src/store/handlers/pushbot.ts b/src/store/handlers/pushbot.ts new file mode 100644 index 00000000..7a8a869b --- /dev/null +++ b/src/store/handlers/pushbot.ts @@ -0,0 +1,165 @@ +/** + * draft/bot-cmds plumbing: + * - subscribes to TAGMSGs that carry +draft/bot-cmds (response to a + * +draft/bot-cmds-query) and caches the decoded schema on the + * server's `botCommands` map keyed by bot nick (lowercased) + * - subscribes to +draft/bot-cmds-changed and clears the cached + * schema for that bot so the next slash invocation re-queries + * - exposes a tiny `queryBotCommands(serverId, botNick)` helper + * used by ChatArea on JOIN to seed the cache for any +B users + * we now share a channel with + */ +import type { StoreApi } from "zustand"; +import { base64DecodeUtf8 } from "../../lib/base64"; +import ircClient from "../../lib/ircClient"; +import type { BotCommand, PushBotInfo } from "../../types"; +import useStore, { type AppState } from "../index"; + +function decodeB64Json(value: string): unknown | null { + try { + return JSON.parse(base64DecodeUtf8(value)); + } catch (e) { + console.warn("[pushbot] base64-JSON decode failed", e); + return null; + } +} + +function decodeBotCmds(value: string): BotCommand[] | null { + const parsed = decodeB64Json(value); + if ( + parsed && + typeof parsed === "object" && + Array.isArray((parsed as { commands?: unknown }).commands) + ) { + return (parsed as { commands: BotCommand[] }).commands; + } + return null; +} + +function decodeBotInfo(value: string): PushBotInfo | null { + const parsed = decodeB64Json(value); + if (parsed && typeof parsed === "object" && (parsed as PushBotInfo).nick) { + return parsed as PushBotInfo; + } + return null; +} + +export function registerPushBotHandlers(store: StoreApi): void { + // When WHO completes for a channel, pre-fetch slash-command schemas + // for any +B users we now share a channel with so the autocomplete + // cache is warm by the time the user types '/'. + ircClient.on("WHO_END", ({ serverId, mask }) => { + if (!mask?.startsWith("#")) return; + const server = store.getState().servers.find((s) => s.id === serverId); + if (!server) return; + const channel = server.channels.find( + (c) => c.name.toLowerCase() === mask.toLowerCase(), + ); + if (!channel) return; + const cache = server.botCommands ?? {}; + for (const u of channel.users) { + if (!u.isBot) continue; + const key = u.username.toLowerCase(); + if (cache[key]) continue; + queryBotCommands(serverId, u.username); + } + }); + + ircClient.on("TAGMSG", (response) => { + const { serverId, sender, mtags } = response; + if (!mtags) return; + + // obby.world/bot-info: server-pushed bot directory entries + // (initial burst + per-bot 'add'/'update'/'remove' events). + // These arrive from the server itself, not from a bot. + if (mtags["obby.world/bot-info"]) { + const info = decodeBotInfo(mtags["obby.world/bot-info"]); + if (!info) return; + const event = info.commands === undefined ? "remove" : "add"; + const evField = (info as unknown as { event?: string }).event ?? event; + const nickKey = info.nick.toLowerCase(); + store.setState((state) => ({ + servers: state.servers.map((s) => { + if (s.id !== serverId) return s; + const next = { ...(s.bots ?? {}) }; + if (evField === "remove") { + delete next[nickKey]; + } else { + next[nickKey] = info; + } + // Keep botCommands in sync so the slash popover picks it up + // without a separate +draft/bot-cmds-query. + const cmds = { ...(s.botCommands ?? {}) }; + if (evField === "remove") { + delete cmds[nickKey]; + } else if (Array.isArray(info.commands)) { + cmds[nickKey] = info.commands; + } + return { ...s, bots: next, botCommands: cmds }; + }), + })); + return; + } + + const botNick = (sender || "").toLowerCase(); + if (!botNick) return; + + if (mtags["+draft/bot-cmds"]) { + const cmds = decodeBotCmds(mtags["+draft/bot-cmds"]); + if (!cmds) return; + store.setState((state) => ({ + servers: state.servers.map((s) => { + if (s.id !== serverId) return s; + const next = { ...(s.botCommands ?? {}), [botNick]: cmds }; + return { ...s, botCommands: next }; + }), + })); + return; + } + + if (mtags["+draft/bot-cmds-changed"]) { + store.setState((state) => ({ + servers: state.servers.map((s) => { + if (s.id !== serverId || !s.botCommands) return s; + if (!(botNick in s.botCommands)) return s; + const next = { ...s.botCommands }; + delete next[botNick]; + return { ...s, botCommands: next }; + }), + })); + // refetch on next slash invocation; UI doesn't need a proactive query + } + }); +} + +/** Send a +draft/bot-cmds-query TAGMSG to (the tag is valueless). */ +export function queryBotCommands(serverId: string, botNick: string): void { + ircClient.sendRaw(serverId, `@+draft/bot-cmds-query TAGMSG ${botNick}`); +} + +/** + * Query bot-cmds for every isBot=true user in the channel that we + * don't already have a cached schema for. Called from the chat input + * the first time the user starts typing a '/' so the popover has + * something to show even if WHO_END fired before our handler attached + * (e.g. cap negotiation happened between joining and registering). + */ +export function queryUncachedBotsInChannel( + serverId: string, + channelName: string, +): void { + const state = useStore.getState(); + const server = state.servers.find((s) => s.id === serverId); + if (!server) return; + const channel = server.channels.find( + (c) => c.name.toLowerCase() === channelName.toLowerCase(), + ); + if (!channel) return; + const cache = server.botCommands ?? {}; + for (const u of channel.users) { + if (!u.isBot) continue; + const key = u.username.toLowerCase(); + if (cache[key]) continue; + queryBotCommands(serverId, u.username); + } +} diff --git a/src/store/index.ts b/src/store/index.ts index 8d6e4cb6..d1f8cfec 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 { BOT_TOOLS_TAG, encodeBotToolsValue } from "../lib/botTools"; import ircClient from "../lib/ircClient"; import { makeLabel } from "../lib/labeledResponse"; import { @@ -477,6 +478,59 @@ interface UIState { export type { GlobalSettings }; +// One step within a draft/bot-tools workflow. `content` is the spec's +// untyped payload: a nested object for tool-call args, a string fragment +// for tool-result / reasoning / text. We carry it through verbatim and +// let the UI decide how to render. +export interface AiStep { + sid: string; + type: import("../lib/botTools").AiStepType; + state: import("../lib/botTools").AiStepState; + tool?: string; + label?: string; + content?: unknown; + truncated?: boolean; + startedAt: number; + updatedAt: number; +} + +export interface AiWorkflow { + id: string; + serverId: string; + // Target the workflow was announced on (channel name or PM nick) so + // the tray can scope cards to the current selection. + channel: string; + // The bot's nick (sender of the original workflow TAGMSG). + senderNick: string; + name?: string; + state: import("../lib/botTools").AiWorkflowState; + // msgid of the IRC message that triggered the workflow, if the bot + // included `trigger` in the start event. Lets the UI correlate. + trigger?: string; + // Truncated copy of the prompt that started the workflow, if the bot + // included it. Rendered under the bot nick on the workflow card so + // the user can see what they asked without scrolling. + prompt?: string; + cancelledBy?: string; + // msgid of the PRIVMSG that carried the final workflow-complete tag. + // Set by the botTools handler when a tagged PRIVMSG arrives so the + // card can deep-link to that message ("Responded in chat" footer). + finalMsgid?: string; + startedAt: number; + updatedAt: number; + steps: AiStep[]; + // UI: card starts collapsed; user can expand. Once dismissed the + // card is hidden from the tray (state is preserved for replay). + collapsed: boolean; + dismissed: boolean; + // True when the workflow was first observed inside a chathistory + // batch (i.e. it's being replayed from the server, not happening + // right now). The floating tray filters these out so joining a + // channel doesn't pop a wall of old workflow cards; they still + // appear in the history popover for inspection. + historical: boolean; +} + export interface AppState { servers: Server[]; currentUser: User | null; @@ -617,6 +671,11 @@ export interface AppState { processedMessageIds: Set; // Set of msgid values that have already been processed // Auto-connect prevention hasConnectedToSavedServers: boolean; + // draft/bot-tools workflow state, indexed by serverId then workflow id. + // Populated by store/handlers/botTools.ts from `+draft/bot-tools` + // tags carried on TAGMSG/PRIVMSG. Rendered by the floating workflow + // tray in ChatArea. + aiWorkflows: Record>; // UI state ui: UIState; globalSettings: GlobalSettings; @@ -948,6 +1007,23 @@ export interface AppState { metadataSubs: (serverId: string) => void; metadataSync: (serverId: string, target: string) => void; sendRaw: (serverId: string, command: string) => void; + // draft/bot-tools UI helpers + control signals. + aiWorkflowSetCollapsed: ( + serverId: string, + workflowId: string, + collapsed: boolean, + ) => void; + aiWorkflowDismiss: (serverId: string, workflowId: string) => void; + // Un-dismiss + expand. Used to reopen a workflow card from its + // final-response PRIVMSG after the user has closed the tray entry. + aiWorkflowReopen: (serverId: string, workflowId: string) => void; + // Send an `action` message (cancel / approve / reject / input) to the + // bot's nick via TAGMSG carrying `+draft/bot-tools`. + aiSendAction: ( + serverId: string, + botNick: string, + action: import("../lib/botTools").AiActionMessage, + ) => void; } // Helper functions for per-server tab selections @@ -1023,6 +1099,7 @@ const useStore = create((set, get) => ({ channelOrder: loadChannelOrder(), processedMessageIds: new Set(), hasConnectedToSavedServers: false, + aiWorkflows: {}, selectedServerId: null, // UI state @@ -3872,6 +3949,69 @@ const useStore = create((set, get) => ({ capAck: (serverId, key, capabilities) => { ircClient.capAck(serverId, key, capabilities); }, + + aiWorkflowSetCollapsed: (serverId, workflowId, collapsed) => { + set((state) => { + const server = state.aiWorkflows[serverId]; + const wf = server?.[workflowId]; + if (!wf) return state; + return { + aiWorkflows: { + ...state.aiWorkflows, + [serverId]: { ...server, [workflowId]: { ...wf, collapsed } }, + }, + }; + }); + }, + + aiWorkflowDismiss: (serverId, workflowId) => { + set((state) => { + const server = state.aiWorkflows[serverId]; + const wf = server?.[workflowId]; + if (!wf) return state; + return { + aiWorkflows: { + ...state.aiWorkflows, + [serverId]: { + ...server, + [workflowId]: { ...wf, dismissed: true }, + }, + }, + }; + }); + }, + + aiWorkflowReopen: (serverId, workflowId) => { + set((state) => { + const server = state.aiWorkflows[serverId]; + const wf = server?.[workflowId]; + if (!wf) return state; + // Clearing `historical` here is deliberate: the flag exists to + // keep the tray quiet on channel-join chathistory replay, but + // once the user has explicitly clicked the inline pill to view + // this run they want the card surfaced even if it was replayed. + return { + aiWorkflows: { + ...state.aiWorkflows, + [serverId]: { + ...server, + [workflowId]: { + ...wf, + dismissed: false, + collapsed: false, + historical: false, + }, + }, + }, + }; + }); + }, + + aiSendAction: (serverId, botNick, action) => { + // base64 of compact JSON; its alphabet needs no IRC tag-value escaping. + const value = encodeBotToolsValue(action); + ircClient.sendRaw(serverId, `@${BOT_TOOLS_TAG}=${value} TAGMSG ${botNick}`); + }, })); // Initialize protocol handlers diff --git a/src/types/index.ts b/src/types/index.ts index 32aecf8b..864b5b2b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -86,6 +86,77 @@ export interface Server { // currently invoke on this server. Used to drive the slash-command // suggestion popover. undefined = the cap is not negotiated. cmdsAvailable?: string[]; + + // draft/bot-cmds: per-bot command schemas keyed by bot nick (lowercased). + // Populated from TAGMSG `+draft/bot-cmds` responses. Used to drive + // slash-command autocomplete + invocation routing. + botCommands?: Record; + + // obby.world/channel-bots: full bot directory. Pushed in a BATCH + // at welcome time, plus per-bot 'add' / 'update' / 'remove' events + // as bots come online / register commands / get suspended. Keyed + // by lowercased nick. + bots?: Record; +} + +export interface PushBotInfo { + bot_id: string; + nick: string; + realname: string; + scope: "channel" | "server"; + transport: "gateway" | "webhook" | "both"; + status: "active" | "pending" | "suspended" | "deleted"; + online: boolean; + from_config: boolean; + channels: string[]; + commands: BotCommand[]; + /** Only present when the receiving user is an oper. */ + webhook_url?: string; + webhook_suspended?: boolean; + webhook_failures?: number; +} + +export interface BotCommandOption { + name: string; + /** Drives the form-element used when the slash-command param modal + * renders this option. Bot authors send the schema; the client + * picks the right control. All types resolve to a string|number| + * boolean value on the wire. */ + type?: + | "string" + | "int" + | "number" + | "bool" + | "user" + | "channel" + | "date" + | "time" + | "datetime" + | "country" + | "password"; + required?: boolean; + description?: string; + choices?: string[]; +} + +/** draft/bot-cmds §Command gating: conditions the invoker must satisfy. */ +export interface BotCommandRequires { + "min-channel-rank"?: "voice" | "halfop" | "op" | "admin" | "owner"; + account?: boolean; + tls?: boolean; +} + +export interface BotCommand { + name: string; + description?: string; + /** Where the command may be invoked (spec schema). */ + contexts?: ("public" | "private" | "pm")[]; + options?: BotCommandOption[]; + requires?: BotCommandRequires; + /** Legacy obby.world/bot-info directory fields, used only as a fallback to + * derive `contexts` when the directory entry predates the spec schema. */ + visibility?: "public" | "private"; + scopes?: ("channel" | "dm")[]; } export interface NamedModeSpec { @@ -256,6 +327,14 @@ export interface Message { replyMessage: Message | null; mentioned: string[]; tags?: Record; + // draft/bot-tools: this message is a synthesised placeholder for a + // live bot workflow that hasn't produced its final PRIVMSG yet, or + // (once the PRIVMSG lands) was morphed from such a placeholder. + // The pill renders off `botToolsWorkflowId`; while `botToolsPending` + // is true the message body is replaced with a workflow-state + // preview (current step, spinner, etc.) instead of plain content. + botToolsWorkflowId?: string; + botToolsPending?: boolean; // Whisper fields (for draft/channel-context) whisperTarget?: string; // The recipient of a whisper // Standard reply fields. `command`, `code`, and `context` are diff --git a/tests/components/SlashParamHint.test.ts b/tests/components/SlashParamHint.test.ts new file mode 100644 index 00000000..9abda66f --- /dev/null +++ b/tests/components/SlashParamHint.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { getActiveParamContext } from "../../src/components/ui/SlashParamHint"; + +describe("getActiveParamContext", () => { + it("returns null while typing the command name", () => { + // cursor at end of "/fore" + expect(getActiveParamContext("/fore", 5)).toBeNull(); + }); + + it("returns null for a literal '/' escape", () => { + expect(getActiveParamContext("//forecast lon", 14)).toBeNull(); + }); + + it("returns argIndex 0 right after the first space", () => { + // "/forecast " cursor at end (10) + expect(getActiveParamContext("/forecast ", 10)).toEqual({ + cmdName: "forecast", + argIndex: 0, + }); + }); + + it("tracks subsequent argument positions", () => { + // "/cmd a b c" with cursor inside the third arg + const input = "/cmd a b c"; + expect(getActiveParamContext(input, input.length)).toEqual({ + cmdName: "cmd", + argIndex: 2, + }); + }); + + it("keeps @botnick targeting in cmdName for composite lookup", () => { + // Caller (SlashParamHint) looks up `forecast@weather` first against + // the schemas map and falls back to the bare `forecast` on miss -- + // letting two bots disambiguate their hint without a shared map key. + expect(getActiveParamContext("/forecast@weather london", 24)).toEqual({ + cmdName: "forecast@weather", + argIndex: 0, + }); + }); + + it("is case-insensitive on the cmd name", () => { + expect(getActiveParamContext("/Forecast london", 16)).toEqual({ + cmdName: "forecast", + argIndex: 0, + }); + }); +}); diff --git a/tests/components/layout/ChatHeader.memberButton.test.tsx b/tests/components/layout/ChatHeader.memberButton.test.tsx index c0bdae8e..08eb2698 100644 --- a/tests/components/layout/ChatHeader.memberButton.test.tsx +++ b/tests/components/layout/ChatHeader.memberButton.test.tsx @@ -116,6 +116,7 @@ describe("ChatHeader - Members Button", () => { onToggleNotificationVolume={() => {}} onOpenChannelSettings={() => {}} onOpenInviteUser={() => {}} + onOpenBots={() => {}} />, ); @@ -153,6 +154,7 @@ describe("ChatHeader - Members Button", () => { onToggleNotificationVolume={() => {}} onOpenChannelSettings={() => {}} onOpenInviteUser={() => {}} + onOpenBots={() => {}} />, ); @@ -193,6 +195,7 @@ describe("ChatHeader - Members Button", () => { onToggleNotificationVolume={() => {}} onOpenChannelSettings={() => {}} onOpenInviteUser={() => {}} + onOpenBots={() => {}} />, ); @@ -231,6 +234,7 @@ describe("ChatHeader - Members Button", () => { onToggleNotificationVolume={() => {}} onOpenChannelSettings={() => {}} onOpenInviteUser={() => {}} + onOpenBots={() => {}} />, ); @@ -269,6 +273,7 @@ describe("ChatHeader - Members Button", () => { onToggleNotificationVolume={() => {}} onOpenChannelSettings={() => {}} onOpenInviteUser={() => {}} + onOpenBots={() => {}} />, ); @@ -297,6 +302,7 @@ describe("ChatHeader - Members Button", () => { onToggleNotificationVolume={() => {}} onOpenChannelSettings={() => {}} onOpenInviteUser={() => {}} + onOpenBots={() => {}} />, ); @@ -323,6 +329,7 @@ describe("ChatHeader - Members Button", () => { onToggleNotificationVolume={() => {}} onOpenChannelSettings={() => {}} onOpenInviteUser={() => {}} + onOpenBots={() => {}} />, ); diff --git a/tests/lib/botTools.test.ts b/tests/lib/botTools.test.ts new file mode 100644 index 00000000..6168c7b7 --- /dev/null +++ b/tests/lib/botTools.test.ts @@ -0,0 +1,165 @@ +import { describe, expect, test } from "vitest"; +import { + decodeBotToolsValue, + encodeBotToolsValue, + escapeIrcTagValue, +} from "../../src/lib/botTools"; + +// Tag values are base64 of compact JSON; wrap test inputs the same way a bot +// would on the wire. +const b64 = (o: unknown): string => + Buffer.from(JSON.stringify(o), "utf8").toString("base64"); + +describe("decodeBotToolsValue", () => { + test("parses a workflow start message with features", () => { + const got = decodeBotToolsValue( + b64({ + msg: "workflow", + id: "7f3a9b", + state: "start", + name: "Research", + trigger: "m0042", + features: ["interactive", "reasoning"], + }), + ); + expect(got).toEqual({ + msg: "workflow", + id: "7f3a9b", + state: "start", + name: "Research", + trigger: "m0042", + features: ["interactive", "reasoning"], + }); + }); + + test("parses a step with nested-object tool-call content", () => { + const got = decodeBotToolsValue( + b64({ + msg: "step", + wid: "7f3a9b", + sid: "s2", + type: "tool-call", + state: "start", + tool: "web-search", + content: { query: "foo" }, + }), + ); + expect(got).toMatchObject({ + msg: "step", + wid: "7f3a9b", + sid: "s2", + type: "tool-call", + tool: "web-search", + content: { query: "foo" }, + }); + }); + + test("parses a reasoning step", () => { + const got = decodeBotToolsValue( + b64({ + msg: "step", + wid: "w", + sid: "s1", + type: "reasoning", + state: "complete", + content: "planning the search", + }), + ); + expect(got).toMatchObject({ + type: "reasoning", + content: "planning the search", + }); + }); + + test("parses an action input message", () => { + const got = decodeBotToolsValue( + b64({ + msg: "action", + action: "input", + target: "7f3a9b", + content: "use the staging server", + }), + ); + expect(got).toEqual({ + msg: "action", + action: "input", + target: "7f3a9b", + content: "use the staging server", + }); + }); + + test("returns null on malformed input", () => { + expect(decodeBotToolsValue("not-valid-base64-!@#")).toBeNull(); + }); + + test("returns null on unknown msg discriminator", () => { + expect(decodeBotToolsValue(b64({ msg: "frob", x: 1 }))).toBeNull(); + }); + + test("returns null on missing required fields", () => { + expect(decodeBotToolsValue(b64({ msg: "workflow", id: "x" }))).toBeNull(); + expect( + decodeBotToolsValue(b64({ msg: "step", wid: "x", sid: "y" })), + ).toBeNull(); + }); + + test("returns null on empty input", () => { + expect(decodeBotToolsValue("")).toBeNull(); + }); + + test("preserves truncated flag and step cancelled-by", () => { + const got = decodeBotToolsValue( + b64({ + msg: "step", + wid: "w", + sid: "s", + type: "tool-result", + state: "cancelled", + content: "part", + truncated: true, + "cancelled-by": "alice", + }), + ); + expect(got).toMatchObject({ truncated: true, "cancelled-by": "alice" }); + }); +}); + +describe("encodeBotToolsValue", () => { + test("emits base64 of compact JSON", () => { + const out = encodeBotToolsValue({ + msg: "workflow", + id: "x", + state: "complete", + }); + expect(out).toBe( + Buffer.from( + '{"msg":"workflow","id":"x","state":"complete"}', + "utf8", + ).toString("base64"), + ); + }); + + test("round-trips through decode", () => { + const original = { + msg: "step" as const, + wid: "w", + sid: "s", + type: "tool-call" as const, + state: "start" as const, + tool: "web-search", + content: { query: "héllo wörld" }, + }; + const re = decodeBotToolsValue(encodeBotToolsValue(original)); + expect(re).toEqual(original); + }); +}); + +describe("escapeIrcTagValue", () => { + test("escapes the five required chars per IRCv3", () => { + expect(escapeIrcTagValue("a;b c\\d\re\nf")).toBe("a\\:b\\sc\\\\d\\re\\nf"); + }); + + test("leaves ordinary text unchanged", () => { + expect(escapeIrcTagValue("hello-world")).toBe("hello-world"); + }); +}); diff --git a/tests/store/botTools.test.ts b/tests/store/botTools.test.ts new file mode 100644 index 00000000..8ae1e74a --- /dev/null +++ b/tests/store/botTools.test.ts @@ -0,0 +1,196 @@ +import { beforeEach, describe, expect, test } from "vitest"; +import ircClient from "../../src/lib/ircClient"; +import type { AppState } from "../../src/store"; +import useStore from "../../src/store"; + +const SID = "srv-1"; +const CHAN = "#ai"; +const BOT = "researchbot"; + +function emit(type: "TAGMSG", payload: Record): void { + // biome-ignore lint/suspicious/noExplicitAny: triggerEvent is dynamically typed + ircClient.triggerEvent(type as any, payload as any); +} + +// The handler decodes base64 tag values; wrap the JSON the way the wire does. +function tag(value: string): Record { + return { "+draft/bot-tools": Buffer.from(value, "utf8").toString("base64") }; +} + +describe("botTools store handler", () => { + beforeEach(() => { + useStore.setState({ aiWorkflows: {} } as Partial); + }); + + test("workflow start creates a new entry with steps:[]", () => { + emit("TAGMSG", { + serverId: SID, + mtags: tag( + '{"msg":"workflow","id":"w1","state":"start","name":"Research","trigger":"m1"}', + ), + sender: BOT, + channelName: CHAN, + }); + const wf = useStore.getState().aiWorkflows[SID]?.w1; + expect(wf).toMatchObject({ + id: "w1", + serverId: SID, + senderNick: BOT, + channel: CHAN, + name: "Research", + state: "start", + trigger: "m1", + steps: [], + collapsed: true, + dismissed: false, + }); + }); + + test("subsequent workflow update merges over existing state", () => { + emit("TAGMSG", { + serverId: SID, + mtags: tag( + '{"msg":"workflow","id":"w1","state":"start","name":"Research"}', + ), + sender: BOT, + channelName: CHAN, + }); + emit("TAGMSG", { + serverId: SID, + mtags: tag('{"msg":"workflow","id":"w1","state":"complete"}'), + sender: BOT, + channelName: CHAN, + }); + const wf = useStore.getState().aiWorkflows[SID]?.w1; + expect(wf?.state).toBe("complete"); + expect(wf?.name).toBe("Research"); // preserved from earlier + }); + + test("step start appends to the workflow's step list", () => { + emit("TAGMSG", { + serverId: SID, + mtags: tag('{"msg":"workflow","id":"w1","state":"start"}'), + sender: BOT, + channelName: CHAN, + }); + emit("TAGMSG", { + serverId: SID, + mtags: tag( + '{"msg":"step","wid":"w1","sid":"s1","type":"tool-call","state":"start","tool":"web-search","content":{"query":"x"}}', + ), + sender: BOT, + channelName: CHAN, + }); + const steps = useStore.getState().aiWorkflows[SID]?.w1.steps; + expect(steps).toHaveLength(1); + expect(steps?.[0]).toMatchObject({ + sid: "s1", + type: "tool-call", + tool: "web-search", + content: { query: "x" }, + state: "start", + }); + }); + + test("step update on same sid merges state without duplicating", () => { + emit("TAGMSG", { + serverId: SID, + mtags: tag('{"msg":"workflow","id":"w1","state":"start"}'), + sender: BOT, + channelName: CHAN, + }); + emit("TAGMSG", { + serverId: SID, + mtags: tag( + '{"msg":"step","wid":"w1","sid":"s1","type":"tool-call","state":"start","tool":"web-search"}', + ), + sender: BOT, + channelName: CHAN, + }); + emit("TAGMSG", { + serverId: SID, + mtags: tag( + '{"msg":"step","wid":"w1","sid":"s1","type":"tool-call","state":"complete","tool":"web-search"}', + ), + sender: BOT, + channelName: CHAN, + }); + const steps = useStore.getState().aiWorkflows[SID]?.w1.steps; + expect(steps).toHaveLength(1); + expect(steps?.[0].state).toBe("complete"); + }); + + test("string content from sequential updates is concatenated", () => { + emit("TAGMSG", { + serverId: SID, + mtags: tag('{"msg":"workflow","id":"w1","state":"start"}'), + sender: BOT, + channelName: CHAN, + }); + emit("TAGMSG", { + serverId: SID, + mtags: tag( + '{"msg":"step","wid":"w1","sid":"s1","type":"tool-result","state":"running","content":"hello "}', + ), + sender: BOT, + channelName: CHAN, + }); + emit("TAGMSG", { + serverId: SID, + mtags: tag( + '{"msg":"step","wid":"w1","sid":"s1","type":"tool-result","state":"running","content":"world"}', + ), + sender: BOT, + channelName: CHAN, + }); + expect(useStore.getState().aiWorkflows[SID]?.w1.steps[0].content).toBe( + "hello world", + ); + }); + + test("step arriving before its workflow is silently dropped", () => { + emit("TAGMSG", { + serverId: SID, + mtags: tag( + '{"msg":"step","wid":"unknown","sid":"s1","type":"reasoning","state":"start"}', + ), + sender: BOT, + channelName: CHAN, + }); + expect(useStore.getState().aiWorkflows[SID]?.unknown).toBeUndefined(); + }); + + test("malformed tag value is ignored", () => { + emit("TAGMSG", { + serverId: SID, + mtags: tag("not-json"), + sender: BOT, + channelName: CHAN, + }); + expect(useStore.getState().aiWorkflows[SID]).toBeUndefined(); + }); + + test("aiWorkflowSetCollapsed flips the UI flag", () => { + emit("TAGMSG", { + serverId: SID, + mtags: tag('{"msg":"workflow","id":"w1","state":"start"}'), + sender: BOT, + channelName: CHAN, + }); + useStore.getState().aiWorkflowSetCollapsed(SID, "w1", false); + expect(useStore.getState().aiWorkflows[SID]?.w1.collapsed).toBe(false); + }); + + test("aiWorkflowDismiss hides without deleting state", () => { + emit("TAGMSG", { + serverId: SID, + mtags: tag('{"msg":"workflow","id":"w1","state":"complete"}'), + sender: BOT, + channelName: CHAN, + }); + useStore.getState().aiWorkflowDismiss(SID, "w1"); + const wf = useStore.getState().aiWorkflows[SID]?.w1; + expect(wf?.dismissed).toBe(true); + expect(wf?.state).toBe("complete"); + }); +}); diff --git a/tests/store/pushbot.test.ts b/tests/store/pushbot.test.ts new file mode 100644 index 00000000..44c1f2bc --- /dev/null +++ b/tests/store/pushbot.test.ts @@ -0,0 +1,174 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import ircClient from "../../src/lib/ircClient"; +import useStore from "../../src/store"; + +function setupServer() { + useStore.setState({ + servers: [ + { + id: "srv-1", + name: "TestServer", + host: "irc.example.com", + port: 6667, + channels: [], + privateChats: [], + isConnected: true, + users: [], + }, + ], + }); +} + +function encodeBotCmds(commands: unknown): string { + const json = JSON.stringify({ commands }); + return Buffer.from(json, "utf8").toString("base64"); +} + +describe("pushbot store handler", () => { + beforeEach(() => { + setupServer(); + }); + + it("caches commands when TAGMSG carries +draft/bot-cmds", () => { + ircClient.triggerEvent("TAGMSG", { + serverId: "srv-1", + sender: "Weather", + channelName: "ourNick", + mtags: { + "+draft/bot-cmds": encodeBotCmds([ + { + name: "forecast", + description: "Look up the weather", + contexts: ["public", "pm"], + }, + ]), + }, + timestamp: new Date(), + }); + + const server = useStore.getState().servers[0]; + expect(server.botCommands).toBeDefined(); + expect(server.botCommands?.weather).toEqual([ + { + name: "forecast", + description: "Look up the weather", + contexts: ["public", "pm"], + }, + ]); + }); + + it("invalidates the cache on +draft/bot-cmds-changed", () => { + useStore.setState((s) => ({ + servers: s.servers.map((srv) => ({ + ...srv, + botCommands: { weather: [{ name: "forecast" }] }, + })), + })); + + ircClient.triggerEvent("TAGMSG", { + serverId: "srv-1", + sender: "Weather", + channelName: "ourNick", + mtags: { "+draft/bot-cmds-changed": "1" }, + timestamp: new Date(), + }); + + const server = useStore.getState().servers[0]; + expect(server.botCommands?.weather).toBeUndefined(); + }); + + it("ignores TAGMSGs without bot-cmds tags", () => { + ircClient.triggerEvent("TAGMSG", { + serverId: "srv-1", + sender: "Weather", + channelName: "#general", + mtags: { "+typing": "active" }, + timestamp: new Date(), + }); + const server = useStore.getState().servers[0]; + expect(server.botCommands).toBeUndefined(); + }); + + // ─── obby.world/channel-bots ────────────────────────────────────── + + function encodeBotInfo(info: object): string { + return Buffer.from(JSON.stringify(info), "utf8").toString("base64"); + } + + it("populates server.bots on obby.world/bot-info 'add'", () => { + ircClient.triggerEvent("TAGMSG", { + serverId: "srv-1", + sender: "obby.example.com", + channelName: "ourNick", + mtags: { + "obby.world/bot-info": encodeBotInfo({ + event: "add", + bot_id: "pb1", + nick: "weather", + realname: "Weather Bot", + scope: "channel", + transport: "gateway", + status: "active", + online: true, + from_config: true, + channels: ["#weather"], + commands: [{ name: "forecast" }], + }), + }, + timestamp: new Date(), + }); + const server = useStore.getState().servers[0]; + expect(server.bots?.weather?.nick).toBe("weather"); + expect(server.bots?.weather?.online).toBe(true); + // also fills botCommands for the slash popover + expect(server.botCommands?.weather?.[0]?.name).toBe("forecast"); + }); + + it("removes server.bots[nick] on obby.world/bot-info 'remove'", () => { + useStore.setState((s) => ({ + servers: s.servers.map((srv) => ({ + ...srv, + bots: { + weather: { + bot_id: "pb1", + nick: "weather", + realname: "Weather Bot", + scope: "channel", + transport: "gateway", + status: "active", + online: true, + from_config: true, + channels: [], + commands: [], + }, + }, + botCommands: { weather: [{ name: "forecast" }] }, + })), + })); + + ircClient.triggerEvent("TAGMSG", { + serverId: "srv-1", + sender: "obby.example.com", + channelName: "ourNick", + mtags: { + "obby.world/bot-info": encodeBotInfo({ + event: "remove", + nick: "weather", + bot_id: "pb1", + realname: "", + scope: "channel", + transport: "gateway", + status: "deleted", + online: false, + from_config: false, + channels: [], + commands: [], + }), + }, + timestamp: new Date(), + }); + const server = useStore.getState().servers[0]; + expect(server.bots?.weather).toBeUndefined(); + expect(server.botCommands?.weather).toBeUndefined(); + }); +});