Skip to content

feat/i18n#171

Merged
matheusfillipe merged 7 commits into
mainfrom
feat/i18n
May 13, 2026
Merged

feat/i18n#171
matheusfillipe merged 7 commits into
mainfrom
feat/i18n

Conversation

@matheusfillipe
Copy link
Copy Markdown
Contributor

@matheusfillipe matheusfillipe commented Apr 18, 2026

Scope

  • Introduce translation system
  • Make it agent friendly and enforced
  • Not introducing RTL languages now... this is complicated in the UI

Translation quality varies and is not always perfect. The focus here is just introducing and making it possible. Refining it can be done gradually later.

Changes

  • Introduce i18n with lingui
  • Multiple translation
  • More texts

Summary by CodeRabbit

  • New Features
    • Expanded full UI localization: English plus CS, DE, ES, FI, FR, IT, JA, KO, NL, PL, PT, RO, RU, SV, TR, UK, ZH‑TW; language selector added to settings; many UI strings, tooltips, buttons, and accessibility labels translated.
  • Documentation
    • Added i18n guidance and workflow for marking translatable strings and adding locales.
  • Chores
    • CI/pre-commit checks added to ensure translation catalogs are extracted/compiled and kept up to date.

@matheusfillipe
Copy link
Copy Markdown
Contributor Author

tiny PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

Adds LinguiJS v5-based internationalization across the app, introduces Lingui CLI config and npm scripts, adds compiled PO catalogs for many locales, updates types and settings to use message descriptors, and enforces catalog freshness via CI and pre-commit hooks.

Changes

i18n integration (UI, core, settings, runtime)

Layer / File(s) Summary
Message runtime + hooks
src/components/**/*, src/components/message/*, src/components/layout/*
Replaced hardcoded user-facing strings with Lingui macros/components (t, Trans, Plural, useLingui) across many UI, layout, and message components; pluralization and locale-aware formatting added where applicable.
Settings types & registry
src/lib/settings/types.ts, src/lib/settings/definitions/allSettings.ts, src/lib/settings/registry.ts
Changed setting text types to MessageDescriptor; wrapped setting strings with msg macro; updated search indexing to use title.id/description?.id; internationalized validation error messages.
Utilities & core behavior
src/lib/eventGrouping.ts, src/lib/irc/IRCClient.ts, src/lib/modal/BaseModal.tsx
Converted event summaries and tooltips to Lingui t/plural; used i18n._ for default QUIT message; localized modal close aria-label.
QuickActions refactor
src/components/ui/QuickActions/uiActionConfig.tsx, src/components/ui/QuickActions.tsx
Replaced exported UI_ACTIONS constant with getUIActions() factory returning i18n-wrapped action descriptors; updated consumers to call getUIActions() and to react to locale changes.
Settings UI rendering
src/components/ui/settings/SettingRenderer.tsx
Added FileInputField wrapper and updated setting render paths to call i18n._(...) for title, description, and placeholder.
User settings & language selector
src/components/ui/UserSettings.tsx
Moved category construction into component and wired a Language selector that persists localStorage and updates i18n.locale/URL; localized many settings labels/descriptions and default message fallbacks.

Tooling, CI, hooks, docs, and catalogs

Layer / File(s) Summary
CI workflow and pre-commit hooks
.github/workflows/workflow.yaml, lefthook.yml
Added i18n GitHub Actions job that runs i18n:extract/i18n:compile and fails on diffs of extracted/compiled catalogs; left a non-failing per-locale coverage report in step summary. Lefthook pre-commit i18n task runs extract/compile and aborts commit if src/locales/ changed; Biome check script updated to re-stage changed staged files when autofixes occur.
npm scripts & dependencies
package.json, lingui.config.ts
Added i18n:extract, i18n:compile, i18n:check, consolidated ci script; added @lingui/* runtime and CLI packages; added lingui.config.ts with sourceLocale, locales, catalog paths, PO formatter and compile namespace.
Locale catalogs & types
src/locales/*, src/locales/catalog.d.ts
Added English source PO (src/locales/en/messages.po) and compiled .mjs catalogs plus many locale .po/.mjs files (e.g., cs, de, es, etc.); added ambient module declaration for */messages.mjs.
Docs & developer guidance
AGENTS.md, ARCHITECTURE.md
Documented Lingui usage patterns (macros, module-scope restrictions), locale resolution precedence, supported locales, catalog workflows, pre-commit expectations, and test mocking of @lingui/react; updated test-count expectation in AGENTS.md.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

"I hopped through strings both big and small,
Wrapped each label, tooltip, and call.
From en to cs, de, es we sing—
Now Obsidian speaks in many a spring.
— 🐇"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/i18n

@github-actions
Copy link
Copy Markdown

Pages Preview
Preview URL: https://feat-i18n.obsidianirc.pages.dev

Automated deployment preview for the PR in the Cloudflare Pages.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (26)
src/components/ui/ColorPicker.tsx (1)

156-156: ⚠️ Potential issue | 🟡 Minor

Untranslated "Close" button label.

The narrow-view modal title was localized, but the desktop view's Close button text at line 156 is still hardcoded English. This is inconsistent with the rest of the i18n migration in this file.

🌐 Proposed fix
-      <button
-        onClick={onClose}
-        className="w-full mt-2 bg-discord-primary text-white py-2 rounded font-medium hover:bg-opacity-80 transition-opacity"
-      >
-        Close
-      </button>
+      <button
+        onClick={onClose}
+        className="w-full mt-2 bg-discord-primary text-white py-2 rounded font-medium hover:bg-opacity-80 transition-opacity"
+      >
+        <Trans>Close</Trans>
+      </button>

And add Trans to the import: import { Trans, t } from "@lingui/macro";

As per coding guidelines: "Wrap all user-visible text with LinguiJS macros for translation: use <Trans> for JSX text children".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/ColorPicker.tsx` at line 156, The desktop "Close" button in
the ColorPicker component is still hardcoded; import Trans from "@lingui/macro"
(alongside any existing t import) and replace the literal "Close" JSX child used
in the desktop/modal close button inside the ColorPicker component with a Trans
wrapper so the label is localizable; ensure you update the import statement and
replace the string at the Close button render site (the Close button JSX in
ColorPicker) with the Trans macro.
src/components/PrivacyPolicy.tsx (1)

9-9: ⚠️ Potential issue | 🟠 Major

Only the image alt was translated — the entire page content is still hardcoded English.

This file is a user-facing Privacy Policy page with many headings, paragraphs, list items, bold labels, and button/footer text (e.g., document.title on line 9, "Privacy Policy" on line 29, "Last updated: December 4, 2025" on line 33, the "Overview"/"Data We Collect"/... section titles, every <p> body, the "Back to App" button, etc.). None of it is wrapped in <Trans> or t\…``, so translations are effectively no-ops here.

If you intend this page to be translatable (which the PR's i18n goal implies), these strings should all be migrated. Otherwise, consider explicitly marking it out-of-scope in the PR description so it isn't missed.

As per coding guidelines: "Wrap all user-visible text with LinguiJS macros for translation: use <Trans> for JSX text children, t`…` for props and variables".

Also applies to: 29-34, 57-67, 91-135, 158-227, 249-287, 309-321, 343-381, 403-433, 455-462, 483-489, 510-515, 537-578, 587-608

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/PrivacyPolicy.tsx` at line 9, The PrivacyPolicy component
currently hardcodes all user-visible strings (e.g., document.title = "Privacy
Policy - ObsidianIRC", the main heading "Privacy Policy", "Last updated:
December 4, 2025", section headings like "Overview"/"Data We Collect", paragraph
bodies, and the "Back to App" button text), so make the page translatable by
wrapping JSX text nodes with <Trans> and using t`…` for prop/variable strings;
update document.title to use i18n.t`Privacy Policy - ObsidianIRC` (or equivalent
from Lingui), replace headings, paragraphs, list items and button labels with
<Trans> blocks, and ensure any attributes (aria-labels, button title, alt text
already done) use t`…` where needed so all user-facing text in the PrivacyPolicy
component is wrapped for Lingui translation.
src/components/message/WhisperMessage.tsx (1)

117-133: ⚠️ Potential issue | 🟠 Major

Fragmented <Trans> segments break translation word order, and the ternary branches are identical.

Two issues here:

  1. Identical ternary branches (lines 118–132): The isOutgoing ? … : … produces the exact same JSX in both arms. Either the "incoming" branch is missing the intended different wording, or the ternary is dead code and should be removed.

  2. Sentence fragmented across multiple <Trans>: Splitting "from <sender> to <recipient>" into separate <Trans>from</Trans> and <Trans>to</Trans> pieces prevents translators from reordering words — many languages cannot preserve English SVO/word order here, and translating isolated prepositions like "from"/"to" leads to incorrect or ambiguous translations (same English word, multiple target-language meanings depending on context). Use a single <Trans> with embedded placeholders so the whole phrase is one translation unit.

🌐 Proposed fix
-          <span className="text-purple-300 font-normal">
-            {isOutgoing ? (
-              <>
-                <Trans>from</Trans>{" "}
-                <span className="font-semibold">{sender}</span>{" "}
-                <Trans>to</Trans>{" "}
-                <span className="font-semibold">{recipient}</span>
-              </>
-            ) : (
-              <>
-                <Trans>from</Trans>{" "}
-                <span className="font-semibold">{sender}</span>{" "}
-                <Trans>to</Trans>{" "}
-                <span className="font-semibold">{recipient}</span>
-              </>
-            )}
-          </span>
+          <span className="text-purple-300 font-normal">
+            <Trans>
+              from <span className="font-semibold">{sender}</span> to{" "}
+              <span className="font-semibold">{recipient}</span>
+            </Trans>
+          </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/message/WhisperMessage.tsx` around lines 117 - 133, The JSX in
WhisperMessage (the span that renders sender/recipient) uses an unnecessary
isOutgoing ternary with identical branches and fragments the sentence into
multiple <Trans> elements; replace the ternary with a single rendering and
consolidate the text into one <Trans> translation unit that accepts placeholders
for sender and recipient (e.g., a single <Trans> like "from {{sender}} to
{{recipient}}" or using Trans's components/values API) so translators can
reorder as needed, while preserving the existing span className
("text-purple-300 font-normal") and the variables sender and recipient.
src/components/ui/AutocompleteDropdown.tsx (1)

281-283: ⚠️ Potential issue | 🟡 Minor

Missed translation: "Members" heading is still hardcoded.

While the modal title and filter placeholder were localized, the section header rendered in the non-narrow dropdown is still a raw English literal and will not be picked up by extraction.

🌐 Proposed fix
         <div className="px-3 py-1 text-xs text-discord-text-muted font-semibold uppercase tracking-wide border-b border-discord-dark-500">
-          Members
+          <Trans>Members</Trans>
         </div>

Add Trans to the existing @lingui/macro import.

As per coding guidelines: "Wrap all user-visible text with LinguiJS macros for translation: use <Trans> for JSX text children".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/AutocompleteDropdown.tsx` around lines 281 - 283, The
"Members" heading in AutocompleteDropdown.tsx is hardcoded; import Trans from
'@lingui/macro' (add it to the existing import list) and replace the raw string
inside the div (the section header in the non-narrow dropdown) with a
<Trans>Members</Trans> JSX node so the text is picked up by Lingui extraction.
src/components/mobile/MessageBottomSheet.tsx (1)

31-40: ⚠️ Potential issue | 🟡 Minor

Use stable action IDs instead of translated labels as keys.

action.label is now translator-controlled, so two actions could translate to the same string and produce duplicate React keys. Add an invariant id for keys.

🔑 Proposed fix
   const actions: {
+    id: string;
     label: string;
     icon: React.ReactNode;
@@
   if (canReply && onReply) {
     actions.push({
+      id: "reply",
       label: t`Reply`,
@@
   if (canReact && onReact) {
     actions.push({
+      id: "react",
       label: t`React`,
@@
   if (canOpenMedia && onOpenMedia) {
     actions.push({
+      id: "open-media",
       label: t`Open in viewer`,
@@
   if (canDelete && onDelete) {
     actions.push({
+      id: "delete",
       label: t`Delete`,
@@
-            key={action.label}
+            key={action.id}

Also applies to: 49-73, 88-91

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mobile/MessageBottomSheet.tsx` around lines 31 - 40, The
actions array in MessageBottomSheet uses translator-controlled label values as
React keys which can collide; add a stable invariant id property to each action
object (e.g., id: 'reply', 'copy', 'delete', etc.) when you push items in the
actions array in the MessageBottomSheet component, and change any mapping that
uses action.label as the key to use action.id instead; update all action
creation sites (the blocks around where actions are pushed and the mapping that
renders them) so every action has a unique id and React keys use that id.
src/components/message/MessageHeader.tsx (1)

30-48: ⚠️ Potential issue | 🟡 Minor

Localize the system label and timestamp locale.

This component now has Lingui context, but Line 35 still formats time with "en-US" and Line 48 still renders "System" directly.

🌐 Proposed fix
-  const { t } = useLingui();
+  const { t, i18n } = useLingui();
@@
-    return new Intl.DateTimeFormat("en-US", {
+    return new Intl.DateTimeFormat(i18n.locale, {
@@
-        {isSystem ? "System" : displayName || username}
+        {isSystem ? t`System` : displayName || username}

As per coding guidelines, Wrap all user-visible text with LinguiJS macros for translation: use <Trans> for JSX text children, t`…` for props and variables, useLingui() for component props.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/message/MessageHeader.tsx` around lines 30 - 48, The code
hardcodes the "System" label and uses a fixed "en-US" locale in formatTime;
update MessageHeader to use Lingui for both: get i18n from useLingui() (const {
t, i18n } = useLingui()), change formatTime to use new
Intl.DateTimeFormat(i18n.locale, ...) instead of "en-US", and render the system
label wrapped with Lingui JSX translation (replace the literal "System" with
<Trans>System</Trans> or t`System` as a JSX child) while leaving
displayName/username logic intact; adjust any imports if needed.
src/components/ui/ImagePreviewModal.tsx (1)

44-46: ⚠️ Potential issue | 🟡 Minor

Wrap the remaining file label for translation.

Line 45 still renders the user-visible "File:" label directly.

🌐 Proposed fix
         <p className="text-sm text-discord-text-muted">
-          File: {file?.name} ({((file?.size || 0) / 1024).toFixed(1)} KB)
+          <Trans>File:</Trans> {file?.name} (
+          {((file?.size || 0) / 1024).toFixed(1)} KB)
         </p>

As per coding guidelines, Wrap all user-visible text with LinguiJS macros for translation: use <Trans> for JSX text children, t`…` for props and variables, useLingui() for component props.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/ImagePreviewModal.tsx` around lines 44 - 46, The visible
label "File:" in the ImagePreviewModal JSX should be wrapped for translation;
update the <p> that renders File: {file?.name} (...) inside the
ImagePreviewModal component to use Lingui (e.g. replace the literal "File:" with
<Trans>File:</Trans> or use t`File:` for props), ensure you import { Trans }
from '@lingui/macro' (or call useLingui() where appropriate), and keep the rest
of the interpolation (file?.name and size formatting) unchanged.
src/components/ui/MiniMediaPlayer.tsx (1)

314-459: ⚠️ Potential issue | 🟠 Major

Several user-visible strings left untranslated.

The accessibility labels were wrapped with t\…``, but the adjacent visible UI text in this file was left as raw English literals and will not be localized:

  • Line 314: Connecting…
  • Line 319: Failed to load
  • Line 347: Live
  • Line 283: fallback "audio" (arguably a label)
  • Line 459: Now Playing

These should also be wrapped so they appear in the catalog and match the surrounding translated labels (note that Connecting is already extracted as the aria-label, so the visible text should reuse it).

🌐 Proposed fix
-              <span className="text-xs truncate">Connecting…</span>
+              <span className="text-xs truncate">{t`Connecting…`}</span>
@@
-              <span className="flex-1 text-xs text-red-400/80 min-w-0 truncate">
-                Failed to load
-              </span>
+              <span className="flex-1 text-xs text-red-400/80 min-w-0 truncate">
+                {t`Failed to load`}
+              </span>
@@
-                  <span className="text-[11px] font-semibold tracking-widest text-red-400 uppercase">
-                    Live
-                  </span>
+                  <span className="text-[11px] font-semibold tracking-widest text-red-400 uppercase">
+                    {t`Live`}
+                  </span>
@@
-            {filename || "Now Playing"}
+            {filename || t`Now Playing`}

As per coding guidelines: “Wrap all user-visible text with LinguiJS macros for translation”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/MiniMediaPlayer.tsx` around lines 314 - 459,
MiniMediaPlayer leaves several visible strings unlocalized; wrap the visible
literals "Connecting…", "Failed to load", "Live" and the fallback "Now Playing"
in the LinguiJS t`...` macro (reuse the existing aria-label for "Connecting…" if
appropriate), and ensure the "audio" fallback passed to playMedia(url, "audio")
is localized if it is user-facing; update the JSX in the MiniMediaPlayer
component (look for the range/controls block, isAudioError block, isAudioLive
branch, play/pause button calls to playMedia, and the filename/Now Playing span)
to call t`...` around those literals so they are extracted into the translation
catalog.
src/components/ui/QuickActions.tsx (1)

341-400: ⚠️ Potential issue | 🟠 Major

Several result titles/descriptions are still hardcoded English and also used as logic.

Inside the settingsRegistry.search block and the user/channel branches you still build user-visible strings with raw template literals:

  • Line 341: title: `Join ${availChannel.channel}`
  • Line 342: description: `${availChannel.userCount} users - ${server.name}`
  • Line 359: title: `Join ${channelName}`
  • Line 360: description: `Join channel on ${server.name}`
  • Line 397: title: `Message ${user.username}`
  • Line 398: description: `Start private message on ${server.name}`

These will not be extracted or translated. Worse, line 545 later does:

const username = result.title.replace("Message ", "");

Once the title is localized (or the prefix ever changes), this parser silently breaks. Derive the username from result.data (carry it explicitly) and wrap the visible strings in t\…``:

🌐 Proposed fix (sketch)
-              results.push({
-                type: "join-channel",
-                id: `join-channel-${server.id}-${availChannel.channel}`,
-                title: `Join ${availChannel.channel}`,
-                description: `${availChannel.userCount} users - ${server.name}`,
+              results.push({
+                type: "join-channel",
+                id: `join-channel-${server.id}-${availChannel.channel}`,
+                title: t`Join ${availChannel.channel}`,
+                description: t`${availChannel.userCount} users - ${server.name}`,
@@
-          results.push({
-            type: "join-channel",
-            id: `join-channel-${server.id}-${channelName}`,
-            title: `Join ${channelName}`,
-            description: `Join channel on ${server.name}`,
+          results.push({
+            type: "join-channel",
+            id: `join-channel-${server.id}-${channelName}`,
+            title: t`Join ${channelName}`,
+            description: t`Join channel on ${server.name}`,
@@
-              results.push({
-                type: "start-dm",
-                id: `start-dm-${server.id}-${user.username}`,
-                title: `Message ${user.username}`,
-                description: `Start private message on ${server.name}`,
+              results.push({
+                type: "start-dm",
+                id: `start-dm-${server.id}-${user.username}`,
+                title: t`Message ${user.username}`,
+                description: t`Start private message on ${server.name}`,
+                // data: keep username so handleSelect doesn't parse title
-                data: undefined,
+                data: { username: user.username },
@@
-        case "start-dm": {
-          if (result.serverId) {
-            const username = result.title.replace("Message ", "");
+        case "start-dm": {
+          if (result.serverId) {
+            const username = (result.data as { username: string }).username;

As per coding guidelines: “Wrap all user-visible text with LinguiJS macros for translation”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/QuickActions.tsx` around lines 341 - 400, The result
objects in the settingsRegistry.search branches build hardcoded
titles/descriptions and later parse usernames from title (e.g., the start-dm
branch creating title `Message ${user.username}` and the parser that does
result.title.replace("Message ", "")), which breaks localization; change all
user-visible strings to use Lingui t`...` macros (wrap the title and description
values in t`...`) and stop deriving logic from localized text by adding explicit
fields to result.data (e.g., include username for "start-dm" results and ensure
channelName exists for join-channel results) and update the downstream code that
extracts the username to read result.data.username instead of parsing
result.title.
src/components/layout/ChannelMessageList.tsx (1)

416-422: ⚠️ Potential issue | 🟡 Minor

Wrap the loading spinner text for extraction.

Loading chat history... is still a user-visible hardcoded string, so it will be missed by Lingui extraction.

Proposed fix
               <LoadingSpinner
                 size="lg"
-                text="Loading chat history..."
+                text={t`Loading chat history...`}
                 className="text-discord-text-muted"
               />

As per coding guidelines, src/components/**/*.{ts,tsx} should wrap all user-visible text with LinguiJS macros.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/ChannelMessageList.tsx` around lines 416 - 422, The
hardcoded user-facing string passed to LoadingSpinner ("Loading chat
history...") must be wrapped with a Lingui macro for extraction; update the JSX
in ChannelMessageList (the LoadingSpinner usage) to pass a translated string
(e.g., t`Loading chat history...` or <Trans>...) instead of the literal, and
ensure you add the corresponding import (import { t } from '@lingui/macro' or
import { Trans } from '@lingui/macro') at the top of the file so Lingui can
extract the message.
src/components/ui/ChannelListModal.tsx (1)

1-17: ⚠️ Potential issue | 🟠 Major

Use useLingui() for the modal’s t labels.

This component computes many JSX props/labels with the imported t macro but never subscribes to Lingui context, so strings like the modal title and placeholders can stay in the previous language after a locale switch until some unrelated state update happens.

Proposed fix
-import { Trans, t } from "@lingui/macro";
+import { Trans, useLingui } from "@lingui/macro";
...
 const ChannelListModal: React.FC = () => {
+  const { t } = useLingui();
   const {
     servers,

As per coding guidelines, JSX props with interpolation should use const { t } = useLingui() inside the component.

Also applies to: 280-305

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/ChannelListModal.tsx` around lines 1 - 17, The component
uses the compile-time t macro imported at module scope, which prevents labels
(modal title, placeholders and other computed JSX props — e.g., the modal title
and input placeholder strings and the other labels around lines referenced) from
updating on locale change; fix by importing useLingui from "@lingui/react",
remove or stop using the top-level t import from "@lingui/macro" for those
runtime UI strings, and inside the ChannelListModal component call const { t } =
useLingui() and use that t for all modal labels/props and the strings mentioned
(including the ones around lines ~280-305) so they subscribe to Lingui context
and update immediately on locale switch.
src/components/message/MessageReactions.tsx (2)

49-50: ⚠️ Potential issue | 🟡 Minor

Localize the tooltip overflow phrase.

and ${rest} more is user-visible and will stay English in non-English locales.

🌐 Proposed fix
 const ReactionTooltip: React.FC<{
   emoji: string;
   users: string[];
   anchor: { x: number; y: number };
 }> = ({ emoji, users, anchor }) => {
+  const { t } = useLingui();
   const ref = useRef<HTMLDivElement>(null);
@@
   const shown = users.slice(0, MAX_TOOLTIP_NAMES);
   const rest = users.length - shown.length;
+  const shownNames = shown.join(", ");
   const names =
-    rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
+    rest > 0 ? t`${shownNames} and ${rest} more` : shownNames;

As per coding guidelines, src/components/**/*.{ts,tsx}: “Wrap all user-visible text with LinguiJS macros for translation: use <Trans> for JSX text children, t`…` for props and variables, useLingui() for component props”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/message/MessageReactions.tsx` around lines 49 - 50, The
tooltip string construction in MessageReactions.tsx currently concatenates
English text via const names = rest > 0 ? `${shown.join(", ")} and ${rest} more`
: shown.join(", `")`; update this to use LinguiJS translation for the
user-visible "and {n} more" phrase: import/use useLingui() or t`` and replace
the template literal so the "{rest} more" fragment is produced via a t`...` call
(or ICU plural if appropriate) and interpolated with rest, ensuring
shown.join(", ") remains unchanged; keep the variable name `names` and the
existing `shown`/`rest` usage while wrapping the translatable portion with
Lingui per project guidelines.

148-155: ⚠️ Potential issue | 🟡 Minor

Add an accessible label to the icon-only reaction buttons.

The localized title helps hover users, but these buttons should also have an explicit accessible name.

♿ Proposed fix
         <button
           type="button"
           className="inline-flex items-center px-2 py-0.5 rounded-full text-sm bg-discord-dark-300 text-discord-channels-default hover:bg-discord-dark-200 hover:text-discord-text-muted transition-all"
           title={t`Add reaction`}
+          aria-label={t`Add reaction`}
           onClick={(e) => onAddReaction(e.currentTarget)}
         >
         <button
           type="button"
           className="inline-flex items-center px-2 py-0.5 rounded-full text-sm bg-discord-dark-300 text-discord-channels-default hover:bg-discord-dark-200 hover:text-discord-text-muted transition-all"
           title={t`Add reaction`}
+          aria-label={t`Add reaction`}
           onClick={(e) => onAddReaction(e.currentTarget)}
         >

Also applies to: 194-201

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/message/MessageReactions.tsx` around lines 148 - 155, The Add
Reaction button is icon-only and only uses title for hover; add an explicit
accessible name by adding an aria-label (or aria-labelledby) to the <button>
that uses the same localized string as title (the t`Add reaction` value) so
screen readers can announce it; update the button that calls onAddReaction (with
MdAddReaction) and do the same for the other icon-only reaction button(s) around
the lines that render other reaction controls (the second block at 194-201) to
ensure all icon-only buttons have aria-labels referencing the localized label.
.github/workflows/workflow.yaml (1)

3-5: ⚠️ Potential issue | 🟡 Minor

Run the i18n check on pull requests as well.

With only push, the catalog gate can be skipped for PR event workflows, especially for forked contributions or required PR checks.

✅ Proposed fix
 on:
   push:
+  pull_request:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/workflow.yaml around lines 3 - 5, The workflow currently
only uses the `on: push` trigger so PRs are not running the i18n/catalog check;
update the workflow trigger block to include `pull_request` (and consider
`pull_request_target` if you need access to repo secrets for forked PRs)
alongside `push` so the catalog gate runs for PR events too; modify the `on`
section to list `push` and `pull_request` (and add `pull_request_target` only if
necessary) so both push and PR events trigger the job.
src/components/ui/AddServerModal.tsx (1)

128-131: ⚠️ Potential issue | 🟡 Minor

Localize the unknown-error fallback.

Unknown error occurred can be rendered to users and is not wrapped for extraction.

🌐 Proposed fix
       const errorMessage =
-        err instanceof Error ? err.message : "Unknown error occurred";
+        err instanceof Error ? err.message : t`Unknown error occurred`;

As per coding guidelines, src/components/**/*.{ts,tsx}: “Wrap all user-visible text with LinguiJS macros for translation”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/AddServerModal.tsx` around lines 128 - 131, The fallback
"Unknown error occurred" in AddServerModal.tsx is not localized; import the
Lingui macro (e.g., import { t } from '@lingui/macro') and replace the literal
fallback with a wrapped message (e.g., use t`Unknown error occurred`) so the
computed errorMessage (used in setError) is localizable; update the catch block
around the err handling in the AddServerModal component accordingly.
src/lib/settings/registry.ts (1)

186-249: ⚠️ Potential issue | 🟠 Major

Search implementation indexes untranslated message descriptor IDs instead of localized text.

Settings titles and descriptions are MessageDescriptor objects; the search index is built using setting.title.id and setting.description?.id (untranslated source text). However, users see localized names in the UI via i18n._(descriptor). This causes non-English users to fail searches in their own language—e.g., a French user cannot search "Surnom" (French for "Nickname").

Additionally, the search index is never rebuilt when the active locale changes, so the issue persists even if users switch languages mid-session.

Pass the active Lingui i18n instance to search() and rebuild the index using translated text, or store both source and translated variants in the search index.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/settings/registry.ts` around lines 186 - 249, The search currently
indexes MessageDescriptor IDs (e.g., setting.title.id, setting.description?.id)
in updateSearchIndex and never rebuilds on locale change, so localized text
isn't searchable; update updateSearchIndex and search to index translated text
via the Lingui i18n instance (use i18n._(setting.title) and
i18n._(setting.description) when building searchableText), accept an i18n
parameter in search (or subscribe to locale changes to rebuild this.searchIndex)
and ensure search() uses the translated index for scoring and matches; update
references to setting.title.id/setting.description?.id in both updateSearchIndex
and the search scoring logic to use the translated strings and rebuild the index
when locale changes.
src/components/message/MediaPreview.tsx (1)

337-341: ⚠️ Potential issue | 🟡 Minor

Translate the remaining media fallback/status labels.

"video", "audio", and Live are visible fallback/status text and will remain English in non-English locales.

🌐 Proposed fix
         <span className="truncate flex-1">
-          {url.split("/").pop()?.split("?")[0] || "video"}
+          {url.split("/").pop()?.split("?")[0] || t`video`}
         </span>
@@
       <span className="text-xs text-discord-text-muted truncate flex-1">
-        {filename || "audio"}
+        {filename || t`audio`}
       </span>
@@
           <span className="text-[10px] font-semibold tracking-widest text-red-400 uppercase">
-            Live
+            <Trans>Live</Trans>
           </span>

As per coding guidelines, src/components/**/*.{ts,tsx}: Wrap all user-visible text with LinguiJS macros for translation.

Also applies to: 727-735

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/message/MediaPreview.tsx` around lines 337 - 341, The file's
MediaPreview component contains user-visible fallback/status strings ("video",
"audio", "Live") that are not wrapped for translation; update the displayed
literals in MediaPreview (the span that renders url.split("/").pop() fallback
and any other occurrences around the component, including the other occurrences
referenced near lines 727-735) to use LinguiJS macros (e.g., t`video`, t`audio`,
t`Live`) so all visible text is localizable, ensuring you import/use the same t
helper already used for the `— open in viewer` string.
src/components/ui/EditServerModal.tsx (1)

119-121: ⚠️ Potential issue | 🟡 Minor

Translate the unknown-error fallback.

The fallback string is rendered in the modal error area, so non-English locales still see English when a non-Error is thrown.

🌐 Proposed fix
       const errorMessage =
-        err instanceof Error ? err.message : "Unknown error occurred";
+        err instanceof Error ? err.message : t`Unknown error occurred`;
       setError(errorMessage);

As per coding guidelines, src/components/**/*.{ts,tsx}: Wrap all user-visible text with LinguiJS macros for translation.

Also applies to: 433-435

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/EditServerModal.tsx` around lines 119 - 121, The fallback
"Unknown error occurred" must be wrapped with LinguiJS translation macro so
non-English locales see a localized message; in EditServerModal.tsx replace the
plain string used to construct errorMessage (the ternary that sets errorMessage
before calling setError) with a translated string using the Lingui macro (e.g.,
t from '@lingui/macro' or equivalent) and ensure the macro is imported at the
top; apply the same change to the other occurrence referenced around the code
block at the second location (lines ~433-435) so any non-Error fallback messages
are also wrapped for translation.
src/components/ui/MediaViewerModal.tsx (1)

167-171: ⚠️ Potential issue | 🟡 Minor

Wrap the remaining visible strings with Lingui.

Live, PDF cannot be displayed in browser., Open in browser, and Loading… are hardcoded and not wrapped with <Trans> macros. As per coding guidelines, src/components/**/*.{ts,tsx} must wrap all user-visible text with LinguiJS macros.

🌐 Proposed fix
           <span className="text-[11px] font-semibold tracking-widest text-red-400 uppercase">
-            Live
+            <Trans>Live</Trans>
           </span>
@@
             <p className="text-sm text-center">
-              PDF cannot be displayed in browser.
+              <Trans>PDF cannot be displayed in browser.</Trans>
             </p>
@@
               <FaExternalLinkAlt className="w-3 h-3" />
-              Open in browser
+              <Trans>Open in browser</Trans>
             </button>
@@
                   <FaSpinner className="animate-spin text-4xl" />
-                  <span className="text-sm">Loading…</span>
+                  <span className="text-sm">
+                    <Trans>Loading…</Trans>
+                  </span>
                 </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/MediaViewerModal.tsx` around lines 167 - 171, The hardcoded
user-visible strings in the MediaViewerModal component (e.g., the "Live" badge
and the messages "PDF cannot be displayed in browser.", "Open in browser", and
"Loading…") must be wrapped with Lingui's <Trans> macro; import { Trans } from
'@lingui/macro' at the top of src/components/ui/MediaViewerModal.tsx if not
present, then replace those literal strings with <Trans>Live</Trans>, <Trans>PDF
cannot be displayed in browser.</Trans>, <Trans>Open in browser</Trans>, and
<Trans>Loading…</Trans> respectively (ensure any surrounding markup/spacing is
preserved and that JSX children remain valid).
src/components/ui/UserProfileModal.tsx (1)

181-185: ⚠️ Potential issue | 🟡 Minor

Localize idle-time units in formatIdleTime().

The function returns untranslated English strings ("seconds", "minutes", "hours", "days"), making the Idle field untranslated in other locales. Use Lingui's plural() macro to handle pluralization:

Example fix
-import { Trans, t } from "@lingui/macro";
+import { Trans, plural, t } from "@lingui/macro";
@@
  const formatIdleTime = (seconds: number): string => {
-   if (seconds < 60) return `${seconds} seconds`;
-   if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`;
-   if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`;
-   return `${Math.floor(seconds / 86400)} days`;
+   if (seconds < 60)
+     return plural(seconds, { one: "# second", other: "# seconds" });
+   const minutes = Math.floor(seconds / 60);
+   if (seconds < 3600)
+     return plural(minutes, { one: "# minute", other: "# minutes" });
+   const hours = Math.floor(seconds / 3600);
+   if (seconds < 86400)
+     return plural(hours, { one: "# hour", other: "# hours" });
+   const days = Math.floor(seconds / 86400);
+   return plural(days, { one: "# day", other: "# days" });
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/UserProfileModal.tsx` around lines 181 - 185, The
formatIdleTime function returns hard-coded English unit strings; replace those
with Lingui plural() so units are localized and pluralized. Import plural from
'@lingui/macro' and for each branch call plural() with the computed unit value
(e.g., seconds, Math.floor(seconds/60), Math.floor(seconds/3600),
Math.floor(seconds/86400)) and appropriate one/other messages (using "#" for
interpolation) so the function returns localized strings; keep the existing
branches and numeric calculations but return the pluralized result instead of
raw English words in formatIdleTime.
src/components/ui/ChannelSettingsModal.tsx (2)

1286-1291: ⚠️ Potential issue | 🟡 Minor

Finish translating the remaining Channel Settings copy.

The advanced tab and mobile category UI still contain hardcoded user-visible strings like “Flood Protection”, “Configure”, “Content Filtering”, “Channel Behavior”, “Channel Settings”, and “entries”. These should be wrapped with <Trans> or t.

As per coding guidelines, src/components/**/*.{ts,tsx}: “Wrap all user-visible text with LinguiJS macros for translation: use <Trans> for JSX text children, t`…` for props and variables, useLingui() for component props”.

Also applies to: 1447-1866, 1955-1987

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/ChannelSettingsModal.tsx` around lines 1286 - 1291, The
ChannelSettingsModal component contains hardcoded user-visible strings (e.g.,
"Flood Protection", "Configure", "Content Filtering", "Channel Behavior",
"Channel Settings", "entries") in JSX and props; replace these with Lingui
macros by wrapping JSX text nodes with <Trans> and using t`...` for
prop/variable strings, and ensure any component-level translations use
useLingui() as needed—update occurrences in the advanced tab and mobile category
UI (and the ranges mentioned around 1447-1866 and 1955-1987) so all visible text
in ChannelSettingsModal.tsx is localized.

1190-1209: ⚠️ Potential issue | 🟡 Minor

Escape the literal {size} token in translated messages.

These strings intend to display {size} literally, but Lingui messages use ICU-style placeholders. Interpolate or escape the token so translation compilation/runtime formatting does not treat it as a missing variable.

Safer interpolation pattern
+  const sizeToken = "{size}";
...
-                    ? t`Upload an image or provide a URL with optional {size} substitution for dynamic sizing`
-                    : t`URL with optional {size} substitution for dynamic sizing. Example: https://example.com/avatar/{size}/channel.jpg`}
+                    ? t`Upload an image or provide a URL with optional ${sizeToken} substitution for dynamic sizing`
+                    : t`URL with optional ${sizeToken} substitution for dynamic sizing. Example: https://example.com/avatar/${sizeToken}/channel.jpg`}
...
-                      placeholder={t`https://example.com/avatar/{size}/channel.jpg`}
+                      placeholder={t`https://example.com/avatar/${sizeToken}/channel.jpg`}
Lingui v5 ICU message syntax literal curly braces escape {size} in t macro
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/ChannelSettingsModal.tsx` around lines 1190 - 1209, The
translated strings in ChannelSettingsModal.tsx render a literal `{size}` but are
written inside Lingui t`` templates and thus treated as ICU placeholders; update
the two t`` usages (the paragraph text and the input placeholder) to emit a
literal "{size}" by escaping the token for Lingui—either double the braces (e.g.
use {{size}} escape form supported by ICU/Lingui) or change to use an explicit
interpolation key (e.g. pass a size param whose value is the literal "{size}")
so the runtime/compile step doesn’t treat it as a missing variable; ensure the
same change is applied where AvatarUpload/currentAvatarUrl/channelAvatar are
referenced so the UI shows the literal token.
src/components/ui/UserSettings.tsx (2)

1363-1433: ⚠️ Potential issue | 🟡 Minor

Translate the remaining account/settings labels.

Strings such as “Connect to a server to manage operator settings.”, “Authenticate on connect”, “Authenticate Now”, and the mobile “User Settings” header are still hardcoded.

As per coding guidelines, src/components/**/*.{ts,tsx}: “Wrap all user-visible text with LinguiJS macros for translation: use <Trans> for JSX text children, t`…` for props and variables, useLingui() for component props”.

Also applies to: 1457-1460

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/UserSettings.tsx` around lines 1363 - 1433, The JSX
contains hardcoded user-visible strings in renderAccountFields (e.g., the server
notice text, the "Authenticate on connect" label, the "Authenticate Now" button
text, and the mobile "User Settings" header referenced near lines ~1457-1460);
wrap these strings with Lingui macros: use <Trans> for inline JSX children and
t`...` for prop values/placeholders, and ensure useLingui() is imported/used if
needed for component props — update TextInput placeholders, label text, checkbox
label, button children, and the header to use <Trans> or t`...` accordingly so
all visible labels are translatable.

880-888: ⚠️ Potential issue | 🟠 Major

Route the privacy link through the external-link warning flow.

This UI action calls openExternalUrl directly for an external URL. Use the app’s ExternalLinkWarningModal flow instead so users get the expected safety prompt before opening the site.

As per coding guidelines, src/components/**/*.{ts,tsx}: “Every user-visible external URL opened by the app must pass through ExternalLinkWarningModal before openExternalUrl is called — never call openExternalUrl directly from UI elements”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/UserSettings.tsx` around lines 880 - 888, Replace the
direct call to openExternalUrl inside the button onClick with the app’s
external-link warning flow: when clicked, set state to open
ExternalLinkWarningModal (e.g., set a url state like pendingExternalUrl and
setShowExternalLinkWarning(true)), and pass the external URL
("https://obsidianirc.pages.dev/privacy") into that state; then only call
openExternalUrl from the modal’s confirm/handler (e.g., ExternalLinkWarningModal
onConfirm should call openExternalUrl(pendingExternalUrl)). Update
UserSettings.tsx to open "/privacy" in a new tab for non-tauri builds via the
same warning modal flow (use the literal URL you currently choose) and remove
any direct calls to openExternalUrl from the button itself.
src/components/ui/settings/SettingRenderer.tsx (2)

327-331: ⚠️ Potential issue | 🟡 Minor

Tooltip text not translated — inconsistent with title/description.

setting.title and setting.description are passed through i18n._(...), but setting.tooltip is rendered as a raw string. Same field type, same PR — this will leave tooltips untranslated.

Proposed fix
           {setting.tooltip && (
             <p className="text-discord-text-muted text-xs italic mt-1">
-              💡 {setting.tooltip}
+              💡 {i18n._(setting.tooltip)}
             </p>
           )}

As per coding guidelines: "Wrap all user-visible text with LinguiJS macros for translation".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/settings/SettingRenderer.tsx` around lines 327 - 331, The
tooltip text is rendered raw while title/description use Lingui i18n, so wrap
the tooltip the same way: replace direct use of setting.tooltip in the
SettingRenderer component with a translated string via i18n._(setting.tooltip)
(or the same translation helper used for setting.title/setting.description),
ensuring the JSX renders the translated value (e.g., inside the <p> block that
currently references setting.tooltip) and handle the case where setting.tooltip
may be undefined/null as the other fields do.

266-278: ⚠️ Potential issue | 🟡 Minor

Fallback branches leak hardcoded English.

"Custom renderer not implemented for {setting.id}" and "Unknown setting type: {setting.type}" are user-visible but not wrapped. Even if they only appear in misconfiguration scenarios, they should go through <Trans> for consistency with the rest of the PR.

Proposed fix
     case "custom":
       return (
         <div className="text-discord-text-muted text-sm">
-          Custom renderer not implemented for {setting.id}
+          <Trans>Custom renderer not implemented for {setting.id}</Trans>
         </div>
       );

     default:
       return (
         <div className="text-red-400 text-sm">
-          Unknown setting type: {setting.type}
+          <Trans>Unknown setting type: {setting.type}</Trans>
         </div>
       );

As per coding guidelines: "Wrap all user-visible text with LinguiJS macros for translation".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/settings/SettingRenderer.tsx` around lines 266 - 278, The
fallback UI in SettingRenderer.tsx currently renders hardcoded English messages
for the "custom" case and the default branch; update both branches (the "custom"
case that returns "Custom renderer not implemented for {setting.id}" and the
default branch that returns "Unknown setting type: {setting.type}") to use
LinguiJS translation macros by wrapping the user-visible strings with <Trans>
(preserving the interpolations like {setting.id} and {setting.type}) so they go
through the app's i18n system; ensure you import the Trans macro where it’s not
already present.
🧹 Nitpick comments (7)
src/components/ui/FloodSettingsModal.tsx (1)

262-268: Label list rebuilt every render — fine, but consider memoizing.

floodProfiles is recomputed on every render since t\…`evaluates at call time. Not a bug (and needed so translations re-evaluate on locale change), but you coulduseMemo` keyed on the active locale if this ever shows up in profiling. Optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/FloodSettingsModal.tsx` around lines 262 - 268,
floodProfiles is being recreated on every render because the t`...` template is
evaluated each time; wrap its creation in a useMemo inside the
FloodSettingsModal component so it only recomputes when the active locale (or
i18n instance) changes. Specifically, memoize the array assigned to
floodProfiles using React.useMemo with the locale/i18n as the dependency so
translations update on locale change but avoid unnecessary rebuilds on other
renders.
src/components/ui/QuickActions.tsx (1)

410-418: Consider destructuring _ directly from useLingui() for the dependency array.

i18n._ is a bound method reference that remains stable across renders. While it works as a dependency, a clearer pattern per Lingui best practices is to destructure the _ function from useLingui(), which has a reference that changes on context updates. Alternatively, you can use i18n.locale to make the intent explicit that recomputation is triggered by locale changes:

Suggested changes (pick one approach)

Approach 1 (recommended): Destructure _ directly

-  const { i18n } = useLingui();
+  const { i18n, _ } = useLingui();

Then in the dependency array:

-  }, [
-    searchQuery,
-    servers,
-    channelList,
-    ui,
-    globalSettings,
-    currentUser,
-    i18n._,
-  ]);
+  }, [
+    searchQuery,
+    servers,
+    channelList,
+    ui,
+    globalSettings,
+    currentUser,
+    _,
+  ]);

Approach 2: Use i18n.locale (explicit intent but may trigger linting)

-  }, [
-    searchQuery,
-    servers,
-    channelList,
-    ui,
-    globalSettings,
-    currentUser,
-    i18n._,
-  ]);
+  }, [
+    searchQuery,
+    servers,
+    channelList,
+    ui,
+    globalSettings,
+    currentUser,
+    i18n.locale,
-  ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/QuickActions.tsx` around lines 410 - 418, The dependency
array currently includes the bound method reference i18n._ which is stable;
instead, update QuickActions to destructure the translation function from
useLingui (e.g., const { _ } = useLingui()) and replace i18n._ with _ in the
effect's dependency array so locale/context changes correctly trigger
recomputation, or alternatively use i18n.locale in the dependency array if you
prefer explicit locale-based updates; modify the dependency list where
searchQuery, servers, channelList, ui, globalSettings, currentUser, i18n._ are
declared to reference the chosen symbol (_ or i18n.locale) and remove i18n._.
src/lib/settings/types.ts (1)

64-69: Extend the descriptor typing to the remaining display metadata.

tooltip, SettingOption.label, and SettingOption.description are also user-visible settings text, but the type still allows raw strings, so untranslated settings can slip through.

🌐 Proposed type tightening
   title: MessageDescriptor;
   description?: MessageDescriptor;
   icon?: ReactNode;
   placeholder?: MessageDescriptor;
-  tooltip?: string;
+  tooltip?: MessageDescriptor;
@@
 export interface SettingOption {
   value: string | number;
-  label: string;
-  description?: string;
+  label: MessageDescriptor;
+  description?: MessageDescriptor;
   icon?: ReactNode;
   disabled?: boolean;
 }

As per coding guidelines, src/components/**/*.{ts,tsx}: “Wrap all user-visible text with LinguiJS macros for translation”.

Also applies to: 116-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/settings/types.ts` around lines 64 - 69, The tooltip field and the
user-facing text fields in SettingOption are currently typed as plain strings,
so change tooltip: string to tooltip: MessageDescriptor and tighten
SettingOption.label and SettingOption.description to MessageDescriptor as well
(match existing title/placeholder types); update any other display-metadata
blocks in this file (the similar group later in the file) to use
MessageDescriptor so all user-visible settings text requires the translation
descriptor type.
src/components/message/MediaPreview.tsx (1)

1-1: Use hook-scoped t for JSX props in this component file.

These aria/title/alt strings are rendered inside React components, so use const { t } = useLingui() in each component that needs prop text instead of a module-imported t. This also lets the component subscribe correctly to locale changes.

🛠️ Directional fix
-import { Trans, t } from "@lingui/macro";
+import { Trans, useLingui } from "@lingui/react/macro";
@@
 const ImagePreview: React.FC<{
@@
 }) => {
+  const { t } = useLingui();
@@
 const VideoUnsupported: React.FC<{
@@
 }> = ({ url, msgid, serverId, channelId }) => {
+  const { t } = useLingui();
@@
 const VideoPlayer: React.FC<{
@@
 }> = ({ url, msgid, serverId, channelId }) => {
+  const { t } = useLingui();

Apply the same local useLingui() pattern to AudioPreview, SoundCloudEmbed, and PdfPreview.

As per coding guidelines, src/components/**/*.{ts,tsx}: Use t`…` via useLingui() for JSX props with interpolation: const { t } = useLingui(); <input placeholder={tMessage #${channelName}} />.

Also applies to: 288-288, 327-341, 577-645, 742-770, 864-864, 993-993

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/message/MediaPreview.tsx` at line 1, The file currently
imports t from "@lingui/macro" and uses it in JSX props; change each component
(MediaPreview, AudioPreview, SoundCloudEmbed, PdfPreview) to obtain t via
hook-scoped const { t } = useLingui() inside the component function and use that
t for all aria/title/alt/placeholder strings and any interpolation, then remove
the module-level import of t from "@lingui/macro" so the components correctly
subscribe to locale changes; ensure you update every occurrence referenced
(lines noted in the review) where JSX props use the module-level t.
src/components/ui/UserProfileModal.tsx (1)

1-1: Use component-scoped t for JSX props.

These title props are user-visible/accessibility text but use the imported macro directly. Prefer const { t } = useLingui() inside UserProfileModal for component props, and pass that translator into helpers if needed. As per coding guidelines, src/components/**/*.{ts,tsx} should use t`…` via useLingui() for component props.

Also applies to: 365-377, 565-581, 713-723

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/UserProfileModal.tsx` at line 1, The component is using the
imported macro t for user-visible JSX props; change to a component-scoped
translator by importing and calling useLingui inside UserProfileModal (const { t
} = useLingui()) and replace occurrences of the imported t in
title/accessibility props (and the other ranges noted) with this local t; if any
helper functions need translation, pass the component-scoped t into them rather
than using the top-level macro.
src/components/ui/MediaViewerModal.tsx (1)

6-6: Use useLingui() for translated JSX props.

This component translates many aria-label, title, and alt props via the imported t macro. Prefer const { t } = useLingui() inside each component that renders those props (AudioViewerPlayer, PdfModalViewer, MediaViewerModal) so prop text follows the active i18n context. As per coding guidelines, src/components/**/*.{ts,tsx} should use t`…` via useLingui() for component props.

Also applies to: 181-200, 348-360, 1214-1323, 1369-1490, 1581-1581

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/MediaViewerModal.tsx` at line 6, The file currently imports
the t macro at module scope and uses it for aria-label/title/alt props; change
these components (MediaViewerModal, AudioViewerPlayer, PdfModalViewer) to obtain
t via the useLingui() hook inside each component (e.g., const { t } =
useLingui()) and replace module-scope t`...` usages in JSX props with the
hook-provided t so translations follow the active i18n context; remove the
top-level import of t and update all occurrences in the ranges called out
(around lines 181-200, 348-360, 1214-1323, 1369-1490, 1581) to use the local
const { t } = useLingui() and t`...` template for props like aria-label, title,
and alt.
src/components/ui/settings/SettingRenderer.tsx (1)

164-211: select/radio option labels and descriptions should use translatable MessageDescriptor type like main setting fields.

SettingOption.label and option.description are typed as plain string, while SettingDefinition.title and description are MessageDescriptor and translated via i18n._(). Per coding guidelines, all user-visible text must be wrapped for translation. The SettingOption interface should be updated to use MessageDescriptor for label and description fields to match the pattern used for main setting fields, ensuring consistency and future-proofing as options are added to actual settings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/settings/SettingRenderer.tsx` around lines 164 - 211,
Update the SettingOption type and rendering so option labels/descriptions are
translatable: change SettingOption.label and SettingOption.description to use
MessageDescriptor (matching SettingDefinition.title/description), then in
SettingRenderer (cases "select" and "radio") call the i18n translation helper
(e.g., i18n._()) when rendering option.label and option.description so the UI
displays translated strings; ensure disabled/checked/value logic remains
unchanged and import MessageDescriptor where needed.

Comment thread lefthook.yml
Comment on lines +20 to +31
run: |
npm run i18n:extract
npm run i18n:compile
# If extract/compile touched any catalog file, stage the updates and abort
# so the new/updated .po and .mjs files ride in the same commit next time.
if ! git diff --exit-code --quiet src/locales/ 2>/dev/null; then
git add src/locales/
echo ""
echo "i18n catalogs updated (new strings found or compiled output changed)."
echo "Catalog changes have been staged. Re-run git commit to proceed."
exit 1
fi
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve i18n command failures in the hook.

If i18n:extract or i18n:compile fails without leaving a src/locales/ diff, this script can still exit successfully. Capture and return the npm exit status after the staging check.

🛠️ Proposed fix
       glob: "src/**/*.{ts,tsx}"
       run: |
-        npm run i18n:extract
-        npm run i18n:compile
+        I18N_EXIT=0
+        npm run i18n:extract || I18N_EXIT=$?
+        if [ "$I18N_EXIT" -eq 0 ]; then
+          npm run i18n:compile || I18N_EXIT=$?
+        fi
         # If extract/compile touched any catalog file, stage the updates and abort
         # so the new/updated .po and .mjs files ride in the same commit next time.
         if ! git diff --exit-code --quiet src/locales/ 2>/dev/null; then
           git add src/locales/
           echo ""
           echo "i18n catalogs updated (new strings found or compiled output changed)."
           echo "Catalog changes have been staged. Re-run git commit to proceed."
           exit 1
         fi
+        exit "$I18N_EXIT"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
run: |
npm run i18n:extract
npm run i18n:compile
# If extract/compile touched any catalog file, stage the updates and abort
# so the new/updated .po and .mjs files ride in the same commit next time.
if ! git diff --exit-code --quiet src/locales/ 2>/dev/null; then
git add src/locales/
echo ""
echo "i18n catalogs updated (new strings found or compiled output changed)."
echo "Catalog changes have been staged. Re-run git commit to proceed."
exit 1
fi
run: |
I18N_EXIT=0
npm run i18n:extract || I18N_EXIT=$?
if [ "$I18N_EXIT" -eq 0 ]; then
npm run i18n:compile || I18N_EXIT=$?
fi
# If extract/compile touched any catalog file, stage the updates and abort
# so the new/updated .po and .mjs files ride in the same commit next time.
if ! git diff --exit-code --quiet src/locales/ 2>/dev/null; then
git add src/locales/
echo ""
echo "i18n catalogs updated (new strings found or compiled output changed)."
echo "Catalog changes have been staged. Re-run git commit to proceed."
exit 1
fi
exit "$I18N_EXIT"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lefthook.yml` around lines 20 - 31, The hook currently runs npm run
i18n:extract and npm run i18n:compile but ignores their exit codes if no
src/locales/ diff is produced; capture the exit status of those commands
(referencing the npm calls npm run i18n:extract and npm run i18n:compile) and
ensure the script exits with that status after the staging-check block (the git
diff / git add / exit 1 sequence) so that failures in extract/compile are
propagated; implement this by saving the exit code(s) (e.g., via a local
variable or using set -o errexit) before the diff check and returning that code
if no catalog changes were staged.

Comment thread src/components/message/InviteMessage.tsx Outdated
Comment thread src/components/message/MessageItem.tsx Outdated
Comment thread src/components/ui/BlankPage.tsx Outdated
Comment thread src/components/ui/GlobalNotifications.tsx Outdated
Comment thread src/components/ui/InviteUserModal.tsx Outdated
@@ -1,3 +1,4 @@
import { Trans, t } from "@lingui/macro";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use useLingui() t for JSX props with interpolation.

Lines 63 and 83 interpolate ${channelName} into a t\…`expression sourced from the module-level@lingui/macroimport. Per the project convention this should come fromuseLingui()so the translation re-evaluates on locale change and the component stays reactive to the active catalog; the module-levelt` binds to the global i18n at import time.

As per coding guidelines: "Use t`…` via useLingui() for JSX props with interpolation: const { t } = useLingui(); <input placeholder={t\Message #${channelName}`} />`".

Proposed fix
-import { Trans, t } from "@lingui/macro";
+import { Trans } from "@lingui/macro";
+import { useLingui } from "@lingui/react/macro";
@@
 }) => {
+  const { t } = useLingui();
   const [username, setUsername] = useState("");

Lines 33, 70, 77 (non-interpolated) can keep using the same hook-provided t for consistency.

Also applies to: 63-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/InviteUserModal.tsx` at line 1, The component is using the
module-level t from `@lingui/macro` for JSX props with interpolation (e.g., the
interpolated strings that reference channelName inside the InviteUserModal
component); replace those uses by calling useLingui() at the top of the
component (const { t } = useLingui()) and use that hook-provided t for all JSX
props that interpolate values (the interpolated t`...${channelName}...`
instances around the input/placeholder/text at the spots identified), and
optionally switch the nearby non-interpolated t usages in InviteUserModal to the
same hook-provided t for consistency so translations re-evaluate on locale
change.

Comment on lines +1539 to +1548
onChange={(e) => {
const next = e.target.value;
localStorage.setItem("locale", next);
const url = new URL(window.location.href);
if (url.searchParams.has("lang")) {
url.searchParams.set("lang", next);
window.location.href = url.toString();
} else {
window.location.reload();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Confirm before reloading for a locale change.

The language selector reloads/navigates immediately, bypassing the existing unsaved-changes guard. If the user has edited settings in this modal, those changes are lost.

Proposed guard/helper extraction
+  const handleLocaleChange = useCallback(
+    (next: string) => {
+      if (
+        hasUnsavedChanges &&
+        !window.confirm(
+          t`You have unsaved changes. Changing language will reload the app and discard them. Continue?`,
+        )
+      ) {
+        return;
+      }
+
+      localStorage.setItem("locale", next);
+      const url = new URL(window.location.href);
+      if (url.searchParams.has("lang")) {
+        url.searchParams.set("lang", next);
+        window.location.href = url.toString();
+      } else {
+        window.location.reload();
+      }
+    },
+    [hasUnsavedChanges],
+  );
...
-                          onChange={(e) => {
-                            const next = e.target.value;
-                            localStorage.setItem("locale", next);
-                            const url = new URL(window.location.href);
-                            if (url.searchParams.has("lang")) {
-                              url.searchParams.set("lang", next);
-                              window.location.href = url.toString();
-                            } else {
-                              window.location.reload();
-                            }
-                          }}
+                          onChange={(e) => handleLocaleChange(e.target.value)}
...
-                        onChange={(e) => {
-                          const next = e.target.value;
-                          localStorage.setItem("locale", next);
-                          const url = new URL(window.location.href);
-                          if (url.searchParams.has("lang")) {
-                            url.searchParams.set("lang", next);
-                            window.location.href = url.toString();
-                          } else {
-                            window.location.reload();
-                          }
-                        }}
+                        onChange={(e) => handleLocaleChange(e.target.value)}

Also applies to: 1723-1732

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/UserSettings.tsx` around lines 1539 - 1548, The locale
onChange handler immediately writes localStorage and triggers navigation/reload
(localStorage.setItem("locale", next"), url.searchParams.set(...),
window.location.href = ..., window.location.reload()), which bypasses the
unsaved-changes guard; update this handler to first invoke the existing
unsaved-changes confirmation flow (e.g., call the app's confirm/guard function
or show a confirmation modal) and only perform the localStorage update and
navigation/reload if the user confirms; apply the same change to the second
occurrence of this handler (the block at the other location) so both places
respect the unsaved-changes guard.

Comment thread src/lib/eventGrouping.ts Outdated
@matheusfillipe matheusfillipe added the DO NOT MERGE Untested, placeholders, just not meant to be merged yet or ever. label Apr 18, 2026
@matheusfillipe
Copy link
Copy Markdown
Contributor Author

@ValwareIRC "DO NOT MERGE"

- Fix View Profile button still rendering in English
- Fix Russian "Вы вышел" grammatical error → "ты вышел"
- Wrap untranslated strings in JsonLogMessage, MediaPreview,
  MiniMediaPlayer, MediaViewerModal, EditServerModal, AddServerModal,
  ChannelList, AvatarUpload, ChannelSettingsModal
- Translate all 46 new strings across 18 non-English locales
- Document agent translation responsibility in CLAUDE.md
@anotherdoesnm
Copy link
Copy Markdown

@ValwareIRC "DO NOT MERGE"

just convert to draft lol

@matheusfillipe
Copy link
Copy Markdown
Contributor Author

matheusfillipe commented Apr 18, 2026

@ValwareIRC "DO NOT MERGE"

just convert to draft lol

Good point but coderabit doesn't review draft by default, and what I mean is this is ready to review but I might change some things so don't merge it just yet.

Just trying to make the process faster.

It is different from "draft" where it means basically "ignore this"

It is also for merge conflict management.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/ui/settings/IgnoreListField.tsx (1)

107-113: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an explicit aria-label to the icon-only remove button.

title alone is not a reliable accessible name for screen readers; this can block keyboard/screen-reader users from understanding the action.

Proposed fix
                 <button
                   type="button"
                   onClick={() => handleRemovePattern(pattern)}
                   disabled={disabled}
                   className="text-red-400 hover:text-red-300 disabled:opacity-50"
+                  aria-label={t`Remove pattern`}
                   title={t`Remove pattern`}
                 >

As per coding guidelines, src/components/**/*.tsx: Use t template literals via useLingui() for JSX props like placeholder=, aria-label=, and title=.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/settings/IgnoreListField.tsx` around lines 107 - 113, The
remove button in IgnoreListField is icon-only and needs an explicit accessible
name: add an aria-label prop to the button (e.g., aria-label={t`Remove
pattern`}) alongside the existing title, using the t template literal from
useLingui(); update the button element where onClick calls
handleRemovePattern(pattern) so screen readers and keyboard users receive a
proper accessible name.
src/components/ui/settings/SettingRenderer.tsx (1)

327-330: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

setting.tooltip is still not localized.

title and description were migrated, but tooltip text is still rendered raw.

As per coding guidelines, "src/**/*.{ts,tsx}: Wrap all user-visible text with LinguiJS macros: use <Trans> for JSX children, t template literals for props and simple strings".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/settings/SettingRenderer.tsx` around lines 327 - 330, The
tooltip text is rendered raw; update the SettingRenderer component to localize
setting.tooltip by wrapping it with LinguiJS macros (use <Trans> around the JSX
child or use t`` for simple strings) when rendering the <p> element that
currently uses setting.tooltip; also add the appropriate import (e.g., import {
Trans } from '@lingui/macro') if missing and ensure any prop passed to the
paragraph is a localized string rather than the raw setting.tooltip value.
🧹 Nitpick comments (6)
src/components/layout/ChatArea.tsx (1)

2156-2174: 💤 Low value

Placeholder branches: functionally correct, but consider hoisting the prefix.

The six t\…`variants for channel vs. private-chat × multiline modes will each be extracted as a separate catalog entry per locale, multiplying translator workload, andselectedChannel.name.replace(/^#/, "")is evaluated every render inside the ternary even when the other branch is taken. Functionally fine, but a smalltarget = selectedChannel ? `#${selectedChannel.name.replace(/^#/, "")}` : `@${selectedPrivateChat.username}`followed by a singlettemplate plus aselect`/conditional suffix would collapse the catalog footprint and read more cleanly. Skip if you prefer to keep the variants explicit for translator context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/ChatArea.tsx` around lines 2156 - 2174, Hoist the
recipient prefix calculation and reduce duplicated t`` templates: compute a
single target string (using selectedChannel and selectedPrivateChat, e.g.,
derive "#{selectedChannel.name.replace(/^#/, '')}" or
"@{selectedPrivateChat.username}") before the JSX, then use one t`` call with a
conditional suffix for the multiline/send hint based on
globalSettings.enableMultilineInput, globalSettings.multilineOnShiftEnter,
isNativeMobile/isMobile and isCompactInput; update the placeholder prop to
reference that single target variable and a single t`` invocation so you avoid
repeated name.replace calls and collapse the six translation variants into one
entry.
AGENTS.md (1)

280-280: 💤 Low value

Add language identifier to fenced code block.

The code block at line 280 is missing a language specifier, which causes markdown linters to flag it and can affect syntax highlighting in rendered docs.

📝 Proposed fix
-```
+```text
 Read /path/to/src/locales/en/messages.po and /path/to/src/locales/{locale}/messages.po.
 Fill in every empty msgstr "" with a {language} translation of the corresponding msgid.
 Rules:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 280, The fenced code block that currently starts with
triple backticks is missing a language identifier; update that backtick fence to
include a language (e.g., change ``` to ```text) so linters and syntax
highlighters recognize the block and the shown instructions (the block
containing "Read /path/to/src/locales/en/messages.po ... Rules:") render
correctly.
src/components/mobile/MessageBottomSheet.tsx (1)

88-98: 💤 Low value

Consider using a stable identifier for React keys.

Using action.label as the key means translated strings serve as reconciliation hints. While functional here (the array is rebuilt on each render and actions are unique), a stable identifier would be more robust and i18n-friendly.

♻️ Proposed refactor

Add an id field to each action object:

   if (canReply && onReply) {
     actions.push({
+      id: 'reply',
       label: t`Reply`,
       icon: <FaReply />,
       onClick: () => {
         onReply();
         onClose();
       },
     });
   }
   
   if (canReact && onReact) {
     actions.push({
+      id: 'react',
       label: t`React`,
       icon: <FaGrinAlt />,
       onClick: (e) => {
         onReact(e.currentTarget);
         onClose();
       },
     });
   }
   
   if (canOpenMedia && onOpenMedia) {
     actions.push({
+      id: 'open-media',
       label: t`Open in viewer`,
       icon: <FaExpand />,
       onClick: () => {
         onOpenMedia();
         onClose();
       },
     });
   }
   
   if (canDelete && onDelete) {
     actions.push({
+      id: 'delete',
       label: t`Delete`,
       icon: <FaTimes />,
       onClick: () => {
         onDelete();
         onClose();
       },
       className: "text-red-400",
     });
   }

Then update the interface and JSX:

   const actions: {
+    id: string;
     label: string;
     icon: React.ReactNode;
     onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
     className?: string;
   }[] = [];
   
   // ...
   
   {actions.map((action) => (
     <button
-      key={action.label}
+      key={action.id}
       className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg active:bg-discord-dark-400 text-left ${action.className || "text-white"}`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/mobile/MessageBottomSheet.tsx` around lines 88 - 98, The keys
in the actions list (used in MessageBottomSheet's JSX where actions.map(...)
uses action.label as key) are unstable because labels may be localized; add a
stable identifier by extending the action type/interface (e.g., add an id:
string to the action objects), update all places that construct the actions
array to provide unique ids, and change the JSX key to use action.id instead of
action.label so React reconciliation uses a stable, i18n-safe key.
.github/workflows/workflow.yaml (1)

36-39: 💤 Low value

Consider using consistent checkout action version.

The i18n job uses actions/checkout@v4 while the biome job uses @v6. While both work, using a consistent version across all jobs reduces maintenance overhead and ensures uniform behavior.

♻️ Proposed fix
       - name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/workflow.yaml around lines 36 - 39, The workflow uses
different checkout action versions between jobs (the i18n job’s "Checkout" step
uses actions/checkout@v4 while the biome job uses `@v6`); standardize them to a
single version (e.g., change the biome job's uses: actions/checkout@v6 to uses:
actions/checkout@v4) by updating every step that matches "uses:
actions/checkout@" across the i18n and biome jobs (and any other jobs) so all
jobs reference the same actions/checkout version.
src/lib/eventGrouping.ts (1)

146-164: ⚡ Quick win

Legacy fallback path contains untranslated strings.

The "Legacy fallback" section (lines 146-164) contains hardcoded English strings like "joined", "left", "quit", "and", and "others" that are not wrapped in Lingui macros. While this may be acceptable if the path is rarely hit, the coding guideline requires all user-visible text to be translatable. Consider wrapping these strings in t macros to maintain consistency, or document why this path can remain English-only.

♻️ Suggested i18n wrapping
   const action =
-    eventType === "join" ? "joined" : eventType === "part" ? "left" : "quit";
+    eventType === "join" ? t`joined` : eventType === "part" ? t`left` : t`quit`;
   if (displayNames.length === 1) {
     const count = usernames.filter((u) => u === unique[0]).length;
     return count > 1
-      ? `${displayNames[0]} ${action} ${count} times`
+      ? plural(count, { one: `${displayNames[0]} ${action} # time`, other: `${displayNames[0]} ${action} # times` })
       : `${displayNames[0]} ${action}`;
   }
   if (displayNames.length === 2)
-    return `${displayNames[0]} and ${displayNames[1]} ${action}`;
+    return t`${displayNames[0]} and ${displayNames[1]} ${action}`;
   if (displayNames.length === 3)
-    return `${displayNames[0]}, ${displayNames[1]} and ${displayNames[2]} ${action}`;
-  return `${displayNames[0]}, ${displayNames[1]} and ${displayNames.length - 2} others ${action}`;
+    return t`${displayNames[0]}, ${displayNames[1]} and ${displayNames[2]} ${action}`;
+  return plural(displayNames.length - 2, {
+    one: t`${displayNames[0]}, ${displayNames[1]} and # other ${action}`,
+    other: t`${displayNames[0]}, ${displayNames[1]} and # others ${action}`,
+  });

Note: The above is illustrative; proper i18n of these composite messages may require restructuring to avoid nested interpolations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/eventGrouping.ts` around lines 146 - 164, The legacy fallback in the
function that reads eventGroup (variables: eventGroup, usernames, eventType,
currentUsername, unique, displayNames, action) uses hardcoded English tokens
("joined"/"left"/"quit", "and", "others", "times") — wrap all user-visible
fragments with Lingui t/plural/Trans macros (or appropriate i18n helpers)
instead of plain strings and compose the final messages using translated
templates and interpolations (handle singular/plural via pluralization helpers
for counts and for the "others" suffix) so every branch (single, two, three,
many) returns fully translatable output.
src/components/message/MessageItem.tsx (1)

157-157: ⚡ Quick win

Remove unused t function from useLingui().

The t function is destructured but never used in this component—only <Trans> and <Plural> appear. If t is genuinely unused, remove the useLingui() call entirely to avoid unnecessary hook overhead.

♻️ Proposed fix
-  const { t } = useLingui();

If you do need t elsewhere in the component, disregard this comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/message/MessageItem.tsx` at line 157, Remove the unused
destructured `t` from the `useLingui()` call in the MessageItem component: the
current line `const { t } = useLingui();` provides `t` but it is never used (the
component only uses `<Trans>` and `<Plural>`), so either remove `t` from the
destructure or remove the `useLingui()` call entirely to avoid unnecessary hook
overhead; update references around the `useLingui()` invocation in the
MessageItem component (search for `useLingui` / `t`) and keep only what the
component needs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Around line 226-233: Update the import paths in the table to use Lingui v5+
split macros: replace `import { Trans } from "@lingui/macro"` with `import {
Trans } from "@lingui/react/macro"`, replace `import { useLingui } from
"@lingui/macro"` with `import { useLingui } from "@lingui/react/macro"`, and
replace `import { t } from "@lingui/macro"` with `import { t } from
"@lingui/core/macro"` (also update the notes about module-level `t` usage
accordingly); ensure the examples mentioning `Trans`, `useLingui`, and `t`
reflect these new import paths.

In `@ARCHITECTURE.md`:
- Around line 310-312: The fenced code block containing the shell command "npm
run i18n:extract && npm run i18n:compile && git add src/locales/" is missing a
language identifier; update the block to include a shell language tag (e.g.,
bash) so it renders with proper syntax highlighting and satisfies MD040, i.e.,
change the opening ``` to ```bash for the block that contains that npm command.
- Around line 281-286: The fenced code block showing the directory structure
(the block containing "src/locales/" and the nested "{locale}/", "messages.po",
"messages.mjs", and "lingui.config.ts") is missing a language identifier; update
that opening fence to include "text" (e.g., ```text) so the block is explicitly
marked as plain text for markdownlint MD040 and better rendering/tooling
support.

In `@src/components/layout/ChatArea.tsx`:
- Around line 98-107: The typing indicator builds a localized string in ChatArea
using a hard-coded English list/grammar via the t`` template (see variable
message and the typingUsers branches), which produces wrong plural forms like "1
others" and prevents proper localization; replace the >3 branch (and ideally the
whole switch) to use Lingui's plural (and/or select) macros so the trailing
count is localized (e.g., plural(typingUsers.length - 3, { one: "1 other",
other: "# others" })) and allow translators to control list conjunctions/order;
ensure all user-visible strings are wrapped with Lingui macros rather than plain
template literals referencing typingUsers usernames.

In `@src/components/message/MessageItem.tsx`:
- Around line 852-856: The Plural usage in MessageItem's JSX uses a template
literal for the other prop which generates distinct message IDs per count;
change the other prop to a single string using Lingui's '#' placeholder (e.g.,
"Show # more items") and keep value={extraKnownEntries.length} (leave the
one="Show 1 more item" as-is) so the runtime substitutes the count without
creating multiple message IDs.

In `@src/components/ui/QuickActions.tsx`:
- Line 719: In QuickActions.tsx replace the hardcoded "DM" return with an
internationalized string: locate the return "DM" inside the QuickActions
component and wrap it using LinguiJS (use t`DM` if it is a simple string/prop or
<Trans>DM</Trans> if it is rendered as JSX children) so the badge matches
sibling badges' i18n pattern; ensure you import the appropriate Lingui symbols
if not already imported.

In `@src/components/ui/settings/IgnoreListField.tsx`:
- Line 83: Replace direct use of t template literals inside JSX children with
the <Trans> component: locate the IgnoreListField component and for each JSX
text child currently written as {t`Add`} (and the other occurrences similar to
lines near the same area) wrap the string with <Trans>…</Trans> instead of using
t directly, and add the appropriate import for Trans from your i18n helper (same
module used by other components) so the button/span/paragraph/heading children
render using <Trans> rather than t.

In `@src/components/ui/settings/SettingRenderer.tsx`:
- Around line 33-35: The FileReader usage (new FileReader(), reader.onload,
reader.readAsDataURL(file)) lacks failure handlers so failed or aborted reads
silently do nothing; add reader.onerror and reader.onabort handlers that surface
failures (e.g., call onChange with null or an empty string OR call a provided
onError callback if available) and optionally log the error; ensure these
handlers reference the same file/read context and clear any temporary state so
the UI gets updated when reader fails or is aborted.
- Around line 26-36: The input allows multiple files (setting.multiple) but the
onChange handler only reads files?.[0]; update the handler in
SettingRenderer.tsx so when setting.multiple is true it reads all selected files
(use FileReader per file and Promise.all to produce an array of data URLs),
setFileName to a combined name/summary (or leave empty), and call onChange with
an array of strings (update SettingValue type/signature as needed); keep the
single-file branch behavior unchanged (call onChange with a single string) to
preserve backwards compatibility.

In `@src/lib/eventGrouping.ts`:
- Around line 44-48: The plural call in eventGrouping.ts uses a template literal
(`joined ${joinCount} times`) which prevents Lingui from extracting a single
translation key; change the `plural(joinCount, { one: "joined", other: ... })`
usage to use the `#` placeholder (e.g. `other: "joined # times"`) so the call
`plural(joinCount, ...)` with `joinCount` as the count generates one extractable
message ID; update the `plural` invocation that references `joinCount`
accordingly.
- Around line 37-41: The pluralization call around reconnectCount is using a
template literal which embeds the number into the message ID; change the `other`
branch to use the `#` placeholder (e.g. "reconnected # times") and ensure you
pass reconnectCount as the count argument to `plural()` so Lingui can extract
proper CLDR forms; update the call that currently references `reconnectCount`
and the message strings "reconnected" / "reconnected # times" accordingly.
- Around line 196-198: The pluralization call inside the map callback ([u, c])
=> `${u}: ${plural(c, { one: "1 time", other: `${c} times` })}` creates unique
message IDs because it injects the count into the string; update the plural()
usage to use the '#' placeholder for the numeric substitution (e.g., plural(c, {
one: "1 time", other: "# times" })) while keeping the outer `${u}: `
interpolation intact so translations extract a stable message id and the count
is substituted at runtime.

In `@src/lib/irc/IRCClient.ts`:
- Around line 857-859: Replace the logical-OR fallback when building the quit
message so explicit empty strings are preserved: in IRCClient.ts update the
assignment that uses quitMessage and i18n._ (the const message currently using
"quitMessage || i18n._(...)") to use the nullish coalescing operator (??)
instead, i.e., fall back to i18n._ only when quitMessage is null or undefined;
keep the same i18n._ call and variable names (quitMessage, message) unchanged
otherwise.

In `@src/locales/nl/messages.mjs`:
- Line 1: Fix the Dutch typos in the translation catalog by updating the strings
for keys "PPqfdA" (currently "Kanaelconfiguratie-instellingen openen" -> correct
to "Kanaalconfiguratie-instellingen openen") and "SkZcl+" (currently
"floodbeveililingsprofiel" -> correct to "floodbeveiligingsprofiel") in the
source translation file or upstream translation source, then rebuild/recompile
the messages bundle (the code that generates messages.mjs / JSON.parse output)
so the corrected translations are emitted into the exported messages.
- Line 1: Update the Dutch catalog entries for the two keys with typos: change
the value for key "PPqfdA" from "Kanaelconfiguratie" to "Kanaalconfiguratie" and
change the value for key "SkZcl+" from "floodbeveililingsprofiel" to
"floodbeveiliging" in the source messages (messages.po), then recompile
translations by running the i18n compile script (npm run i18n:compile).

---

Outside diff comments:
In `@src/components/ui/settings/IgnoreListField.tsx`:
- Around line 107-113: The remove button in IgnoreListField is icon-only and
needs an explicit accessible name: add an aria-label prop to the button (e.g.,
aria-label={t`Remove pattern`}) alongside the existing title, using the t
template literal from useLingui(); update the button element where onClick calls
handleRemovePattern(pattern) so screen readers and keyboard users receive a
proper accessible name.

In `@src/components/ui/settings/SettingRenderer.tsx`:
- Around line 327-330: The tooltip text is rendered raw; update the
SettingRenderer component to localize setting.tooltip by wrapping it with
LinguiJS macros (use <Trans> around the JSX child or use t`` for simple strings)
when rendering the <p> element that currently uses setting.tooltip; also add the
appropriate import (e.g., import { Trans } from '@lingui/macro') if missing and
ensure any prop passed to the paragraph is a localized string rather than the
raw setting.tooltip value.

---

Nitpick comments:
In @.github/workflows/workflow.yaml:
- Around line 36-39: The workflow uses different checkout action versions
between jobs (the i18n job’s "Checkout" step uses actions/checkout@v4 while the
biome job uses `@v6`); standardize them to a single version (e.g., change the
biome job's uses: actions/checkout@v6 to uses: actions/checkout@v4) by updating
every step that matches "uses: actions/checkout@" across the i18n and biome jobs
(and any other jobs) so all jobs reference the same actions/checkout version.

In `@AGENTS.md`:
- Line 280: The fenced code block that currently starts with triple backticks is
missing a language identifier; update that backtick fence to include a language
(e.g., change ``` to ```text) so linters and syntax highlighters recognize the
block and the shown instructions (the block containing "Read
/path/to/src/locales/en/messages.po ... Rules:") render correctly.

In `@src/components/layout/ChatArea.tsx`:
- Around line 2156-2174: Hoist the recipient prefix calculation and reduce
duplicated t`` templates: compute a single target string (using selectedChannel
and selectedPrivateChat, e.g., derive "#{selectedChannel.name.replace(/^#/,
'')}" or "@{selectedPrivateChat.username}") before the JSX, then use one t``
call with a conditional suffix for the multiline/send hint based on
globalSettings.enableMultilineInput, globalSettings.multilineOnShiftEnter,
isNativeMobile/isMobile and isCompactInput; update the placeholder prop to
reference that single target variable and a single t`` invocation so you avoid
repeated name.replace calls and collapse the six translation variants into one
entry.

In `@src/components/message/MessageItem.tsx`:
- Line 157: Remove the unused destructured `t` from the `useLingui()` call in
the MessageItem component: the current line `const { t } = useLingui();`
provides `t` but it is never used (the component only uses `<Trans>` and
`<Plural>`), so either remove `t` from the destructure or remove the
`useLingui()` call entirely to avoid unnecessary hook overhead; update
references around the `useLingui()` invocation in the MessageItem component
(search for `useLingui` / `t`) and keep only what the component needs.

In `@src/components/mobile/MessageBottomSheet.tsx`:
- Around line 88-98: The keys in the actions list (used in MessageBottomSheet's
JSX where actions.map(...) uses action.label as key) are unstable because labels
may be localized; add a stable identifier by extending the action type/interface
(e.g., add an id: string to the action objects), update all places that
construct the actions array to provide unique ids, and change the JSX key to use
action.id instead of action.label so React reconciliation uses a stable,
i18n-safe key.

In `@src/lib/eventGrouping.ts`:
- Around line 146-164: The legacy fallback in the function that reads eventGroup
(variables: eventGroup, usernames, eventType, currentUsername, unique,
displayNames, action) uses hardcoded English tokens ("joined"/"left"/"quit",
"and", "others", "times") — wrap all user-visible fragments with Lingui
t/plural/Trans macros (or appropriate i18n helpers) instead of plain strings and
compose the final messages using translated templates and interpolations (handle
singular/plural via pluralization helpers for counts and for the "others"
suffix) so every branch (single, two, three, many) returns fully translatable
output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 74c3360e-afd7-4bd5-b505-4c584f16c77b

📥 Commits

Reviewing files that changed from the base of the PR and between fa20aea and 9ced03c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (109)
  • .github/workflows/workflow.yaml
  • AGENTS.md
  • ARCHITECTURE.md
  • lefthook.yml
  • package.json
  • src/components/PrivacyPolicy.tsx
  • src/components/layout/ChannelList.tsx
  • src/components/layout/ChannelMessageList.tsx
  • src/components/layout/ChatArea.tsx
  • src/components/layout/ChatHeader.tsx
  • src/components/layout/MemberList.tsx
  • src/components/layout/ServerList.tsx
  • src/components/message/ActionMessage.tsx
  • src/components/message/EventMessage.tsx
  • src/components/message/InviteMessage.tsx
  • src/components/message/JsonLogMessage.tsx
  • src/components/message/LinkPreview.tsx
  • src/components/message/MediaPreview.tsx
  • src/components/message/MessageItem.tsx
  • src/components/message/MessageReactions.tsx
  • src/components/message/ServerNoticesPopup.tsx
  • src/components/message/WhisperMessage.tsx
  • src/components/mobile/MessageBottomSheet.tsx
  • src/components/mobile/ServerBottomSheet.tsx
  • src/components/ui/AddPrivateChatModal.tsx
  • src/components/ui/AddServerModal.tsx
  • src/components/ui/AutocompleteDropdown.tsx
  • src/components/ui/AvatarUpload.tsx
  • src/components/ui/BlankPage.tsx
  • src/components/ui/ChannelListModal.tsx
  • src/components/ui/ChannelSettingsModal.tsx
  • src/components/ui/ColorPicker.tsx
  • src/components/ui/EditServerModal.tsx
  • src/components/ui/ExternalLinkWarningModal.tsx
  • src/components/ui/FloodSettingsModal.tsx
  • src/components/ui/GifSelector.tsx
  • src/components/ui/GlobalNotifications.tsx
  • src/components/ui/HomeScreen.tsx
  • src/components/ui/ImagePreviewModal.tsx
  • src/components/ui/InputToolbar.tsx
  • src/components/ui/InviteUserModal.tsx
  • src/components/ui/LinkSecurityWarningModal.tsx
  • src/components/ui/MediaCommentsSidebar.tsx
  • src/components/ui/MediaViewerModal.tsx
  • src/components/ui/MiniMediaPlayer.tsx
  • src/components/ui/ModerationModal.tsx
  • src/components/ui/QuickActions.tsx
  • src/components/ui/QuickActions/uiActionConfig.tsx
  • src/components/ui/ReactionModal.tsx
  • src/components/ui/ScrollToBottomButton.tsx
  • src/components/ui/TopicModal.tsx
  • src/components/ui/UserContextMenu.tsx
  • src/components/ui/UserProfileModal.tsx
  • src/components/ui/UserSettings.tsx
  • src/components/ui/settings/CustomMentionsField.tsx
  • src/components/ui/settings/IgnoreListField.tsx
  • src/components/ui/settings/SettingRenderer.tsx
  • src/lib/eventGrouping.ts
  • src/lib/irc/IRCClient.ts
  • src/lib/modal/BaseModal.tsx
  • src/lib/settings/definitions/allSettings.ts
  • src/lib/settings/registry.ts
  • src/locales/catalog.d.ts
  • src/locales/cs/messages.mjs
  • src/locales/cs/messages.po
  • src/locales/de/messages.mjs
  • src/locales/de/messages.po
  • src/locales/en/messages.mjs
  • src/locales/en/messages.po
  • src/locales/es/messages.mjs
  • src/locales/es/messages.po
  • src/locales/fi/messages.mjs
  • src/locales/fi/messages.po
  • src/locales/fr/messages.mjs
  • src/locales/fr/messages.po
  • src/locales/it/messages.mjs
  • src/locales/it/messages.po
  • src/locales/ja/messages.mjs
  • src/locales/ja/messages.po
  • src/locales/ko/messages.mjs
  • src/locales/ko/messages.po
  • src/locales/nl/messages.mjs
  • src/locales/nl/messages.po
  • src/locales/pl/messages.mjs
  • src/locales/pl/messages.po
  • src/locales/pt/messages.mjs
  • src/locales/pt/messages.po
  • src/locales/ro/messages.mjs
  • src/locales/ro/messages.po
  • src/locales/ru/messages.mjs
  • src/locales/ru/messages.po
  • src/locales/sv/messages.mjs
  • src/locales/sv/messages.po
  • src/locales/tr/messages.mjs
  • src/locales/tr/messages.po
  • src/locales/uk/messages.mjs
  • src/locales/uk/messages.po
  • src/locales/zh-TW/messages.mjs
  • src/locales/zh-TW/messages.po
  • src/locales/zh/messages.mjs
  • src/locales/zh/messages.po
  • src/protocol/mode.ts
  • src/store/handlers/auth.ts
  • src/store/handlers/batches.ts
  • src/store/handlers/channels.ts
  • src/store/handlers/users.ts
  • src/store/index.ts
  • tests/lib/eventGrouping.test.ts
  • vite.config.ts
✅ Files skipped from review due to trivial changes (15)
  • src/locales/fi/messages.mjs
  • src/locales/ro/messages.mjs
  • src/locales/catalog.d.ts
  • src/locales/pt/messages.mjs
  • src/locales/uk/messages.mjs
  • src/locales/zh-TW/messages.mjs
  • src/locales/ko/messages.mjs
  • src/components/message/LinkPreview.tsx
  • src/components/ui/AutocompleteDropdown.tsx
  • src/locales/tr/messages.mjs
  • src/locales/pl/messages.mjs
  • src/locales/es/messages.mjs
  • src/components/PrivacyPolicy.tsx
  • src/lib/modal/BaseModal.tsx
  • src/components/ui/MediaCommentsSidebar.tsx
🚧 Files skipped from review as they are similar to previous changes (45)
  • src/components/ui/ColorPicker.tsx
  • src/components/ui/ScrollToBottomButton.tsx
  • src/components/ui/ReactionModal.tsx
  • src/components/mobile/ServerBottomSheet.tsx
  • src/components/ui/InputToolbar.tsx
  • src/components/message/ActionMessage.tsx
  • src/locales/de/messages.mjs
  • src/components/ui/settings/CustomMentionsField.tsx
  • src/components/ui/GifSelector.tsx
  • src/components/ui/InviteUserModal.tsx
  • src/components/ui/AddPrivateChatModal.tsx
  • src/components/ui/ExternalLinkWarningModal.tsx
  • src/components/ui/ImagePreviewModal.tsx
  • src/components/message/EventMessage.tsx
  • src/components/ui/ModerationModal.tsx
  • src/components/ui/AvatarUpload.tsx
  • src/components/message/ServerNoticesPopup.tsx
  • src/components/message/WhisperMessage.tsx
  • src/components/layout/ChannelMessageList.tsx
  • src/components/message/MessageReactions.tsx
  • src/components/layout/ServerList.tsx
  • src/components/ui/GlobalNotifications.tsx
  • src/components/ui/LinkSecurityWarningModal.tsx
  • src/locales/en/messages.mjs
  • src/components/ui/TopicModal.tsx
  • src/locales/cs/messages.mjs
  • src/components/ui/AddServerModal.tsx
  • src/components/ui/FloodSettingsModal.tsx
  • lefthook.yml
  • src/components/ui/ChannelListModal.tsx
  • src/components/layout/MemberList.tsx
  • src/components/layout/ChannelList.tsx
  • src/components/ui/MiniMediaPlayer.tsx
  • src/components/message/JsonLogMessage.tsx
  • src/lib/settings/registry.ts
  • package.json
  • src/components/ui/QuickActions/uiActionConfig.tsx
  • src/components/message/MediaPreview.tsx
  • src/components/ui/UserContextMenu.tsx
  • src/components/layout/ChatHeader.tsx
  • src/components/ui/ChannelSettingsModal.tsx
  • src/lib/settings/definitions/allSettings.ts
  • src/components/ui/UserProfileModal.tsx
  • src/components/ui/MediaViewerModal.tsx
  • src/components/ui/UserSettings.tsx

Comment thread AGENTS.md
Comment on lines +226 to +233
| String location | Macro | Import |
|-----------------|-------|--------|
| JSX text children (`<button>`, `<span>`, `<p>`, headings) | `<Trans>…</Trans>` | `import { Trans } from "@lingui/macro"` |
| JSX props: `placeholder=`, `aria-label=`, `title=` | `` t`…` `` via `useLingui` | `import { useLingui } from "@lingui/macro"` then `const { t } = useLingui()` inside the component |
| Simple `t` outside JSX (inside a render function) | `` t`…` `` | `import { t } from "@lingui/macro"` |
| Variables/interpolation | `` t`Hello ${name}` `` | same — placeholders become `{0}` in the PO file |
| Non-React `.ts` files (store handlers, event callbacks) | `` t`…` `` | `import { t } from "@lingui/macro"` — safe inside callbacks that fire after `i18n.activate()` |
| Module-level constants | **Do not use** `t` at module scope | `t` evaluates before `i18n.activate()` runs. Move the string inside the function body. |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update import paths to match LinguiJS v5+ conventions.

The table references the deprecated @lingui/macro entry point, but the codebase (and the past review comment in BlankPage.tsx) confirms the project uses the v5+ split imports: @lingui/react/macro for React components and @lingui/core/macro for plain JavaScript. This documentation mismatch will confuse developers trying to add new translatable strings.

📝 Proposed fix

Update the "Import" column in the table:

 | String location | Macro | Import |
 |-----------------|-------|--------|
-| JSX text children (`<button>`, `<span>`, `<p>`, headings) | `<Trans>…</Trans>` | `import { Trans } from "@lingui/macro"` |
-| JSX props: `placeholder=`, `aria-label=`, `title=` | `` t`…` `` via `useLingui` | `import { useLingui } from "@lingui/macro"` then `const { t } = useLingui()` inside the component |
-| Simple `t` outside JSX (inside a render function) | `` t`…` `` | `import { t } from "@lingui/macro"` |
+| JSX text children (`<button>`, `<span>`, `<p>`, headings) | `<Trans>…</Trans>` | `import { Trans } from "@lingui/react/macro"` |
+| JSX props: `placeholder=`, `aria-label=`, `title=` | `` t`…` `` via `useLingui` | `import { useLingui } from "@lingui/react/macro"` then `const { t } = useLingui()` inside the component |
+| Simple `t` outside JSX (inside a render function) | `` t`…` `` | `import { t } from "@lingui/react/macro"` or `import { t } from "@lingui/core/macro"` |
 | Variables/interpolation | `` t`Hello ${name}` `` | same — placeholders become `{0}` in the PO file |
-| Non-React `.ts` files (store handlers, event callbacks) | `` t`…` `` | `import { t } from "@lingui/macro"` — safe inside callbacks that fire after `i18n.activate()` |
+| Non-React `.ts` files (store handlers, event callbacks) | `` t`…` `` | `import { t } from "@lingui/core/macro"` — safe inside callbacks that fire after `i18n.activate()` |
 | Module-level constants | **Do not use** `t` at module scope | `t` evaluates before `i18n.activate()` runs. Move the string inside the function body. |

Also update line 242:

 // Props with interpolation (requires useLingui inside the component)
-import { useLingui } from "@lingui/macro";
+import { useLingui } from "@lingui/react/macro";
 const { t } = useLingui();
 <input placeholder={t`Message #${channelName}`} />

And line 247:

 // Simple t tag inside render
-import { t } from "@lingui/macro";
+import { t } from "@lingui/react/macro";
 const label = t`Settings`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| String location | Macro | Import |
|-----------------|-------|--------|
| JSX text children (`<button>`, `<span>`, `<p>`, headings) | `<Trans>…</Trans>` | `import { Trans } from "@lingui/macro"` |
| JSX props: `placeholder=`, `aria-label=`, `title=` | `` t`…` `` via `useLingui` | `import { useLingui } from "@lingui/macro"` then `const { t } = useLingui()` inside the component |
| Simple `t` outside JSX (inside a render function) | `` t`…` `` | `import { t } from "@lingui/macro"` |
| Variables/interpolation | `` t`Hello ${name}` `` | same — placeholders become `{0}` in the PO file |
| Non-React `.ts` files (store handlers, event callbacks) | `` t`…` `` | `import { t } from "@lingui/macro"` — safe inside callbacks that fire after `i18n.activate()` |
| Module-level constants | **Do not use** `t` at module scope | `t` evaluates before `i18n.activate()` runs. Move the string inside the function body. |
| String location | Macro | Import |
|-----------------|-------|--------|
| JSX text children (`<button>`, `<span>`, `<p>`, headings) | `<Trans>…</Trans>` | `import { Trans } from "@lingui/react/macro"` |
| JSX props: `placeholder=`, `aria-label=`, `title=` | `` t`…` `` via `useLingui` | `import { useLingui } from "@lingui/react/macro"` then `const { t } = useLingui()` inside the component |
| Simple `t` outside JSX (inside a render function) | `` t`…` `` | `import { t } from "@lingui/react/macro"` or `import { t } from "@lingui/core/macro"` |
| Variables/interpolation | `` t`Hello ${name}` `` | same — placeholders become `{0}` in the PO file |
| Non-React `.ts` files (store handlers, event callbacks) | `` t`…` `` | `import { t } from "@lingui/core/macro"` — safe inside callbacks that fire after `i18n.activate()` |
| Module-level constants | **Do not use** `t` at module scope | `t` evaluates before `i18n.activate()` runs. Move the string inside the function body. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 226 - 233, Update the import paths in the table to
use Lingui v5+ split macros: replace `import { Trans } from "@lingui/macro"`
with `import { Trans } from "@lingui/react/macro"`, replace `import { useLingui
} from "@lingui/macro"` with `import { useLingui } from "@lingui/react/macro"`,
and replace `import { t } from "@lingui/macro"` with `import { t } from
"@lingui/core/macro"` (also update the notes about module-level `t` usage
accordingly); ensure the examples mentioning `Trans`, `useLingui`, and `t`
reflect these new import paths.

Comment thread ARCHITECTURE.md
Comment on lines +281 to +286
```
src/locales/
{locale}/
messages.po # Human-readable source of truth for translators
messages.mjs # Compiled binary catalog (generated, do not edit)
lingui.config.ts # Locale list, PO format, catalog paths
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Specify language for fenced code block.

The code block showing the directory structure should have a language identifier for better rendering and tooling support.

📝 Proposed fix
-```
+```text
 src/locales/
   {locale}/

As per coding guidelines, this aligns with the markdownlint-cli2 rule MD040.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 281-281: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ARCHITECTURE.md` around lines 281 - 286, The fenced code block showing the
directory structure (the block containing "src/locales/" and the nested
"{locale}/", "messages.po", "messages.mjs", and "lingui.config.ts") is missing a
language identifier; update that opening fence to include "text" (e.g., ```text)
so the block is explicitly marked as plain text for markdownlint MD040 and
better rendering/tooling support.

Comment thread ARCHITECTURE.md
Comment on lines +310 to +312
```
npm run i18n:extract && npm run i18n:compile && git add src/locales/
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Specify language for fenced code block.

The code block showing the shell command should have a language identifier for better rendering and syntax highlighting.

📝 Proposed fix
-```
+```bash
 npm run i18n:extract && npm run i18n:compile && git add src/locales/

As per coding guidelines, this aligns with the markdownlint-cli2 rule MD040.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
npm run i18n:extract && npm run i18n:compile && git add src/locales/
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 310-310: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ARCHITECTURE.md` around lines 310 - 312, The fenced code block containing the
shell command "npm run i18n:extract && npm run i18n:compile && git add
src/locales/" is missing a language identifier; update the block to include a
shell language tag (e.g., bash) so it renders with proper syntax highlighting
and satisfies MD040, i.e., change the opening ``` to ```bash for the block that
contains that npm command.

Comment on lines 98 to 107
let message = "";
if (typingUsers.length === 1) {
message = `${typingUsers[0].username} is typing...`;
message = t`${typingUsers[0].username} is typing...`;
} else if (typingUsers.length === 2) {
message = `${typingUsers[0].username} and ${typingUsers[1].username} are typing...`;
message = t`${typingUsers[0].username} and ${typingUsers[1].username} are typing...`;
} else if (typingUsers.length === 3) {
message = `${typingUsers[0].username}, ${typingUsers[1].username} and ${typingUsers[2].username} are typing...`;
message = t`${typingUsers[0].username}, ${typingUsers[1].username} and ${typingUsers[2].username} are typing...`;
} else if (typingUsers.length > 3) {
message = `${typingUsers[0].username}, ${typingUsers[1].username}, ${typingUsers[2].username} and ${typingUsers.length - 3} others are typing...`;
message = t`${typingUsers[0].username}, ${typingUsers[1].username}, ${typingUsers[2].username} and ${typingUsers.length - 3} others are typing...`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Typing indicator: 1 others grammar bug and hard-coded English plural / list structure.

When typingUsers.length === 4, the last branch produces "…, …, … and 1 others are typing..." — the noun is unconditionally plural. More broadly, the four branches hard-code English comma/and structure and treat the trailing count as always plural, which Lingui's t template literals cannot adjust per locale (each variant is extracted as a fixed string with positional placeholders like "{0}, {1}, {2} and {3} others are typing..."). Many languages need different plural categories (CLDR one/few/many/other) for the trailing count, and some need different list joiners.

Recommend using the plural macro for the >3 branch (and ideally select over the count for the full message) so translators can localize both the conjunction order and the singular/plural form of "other(s)".

♻️ Sketch using Lingui's plural macro for the trailing count
-import { Trans, useLingui } from "@lingui/react/macro";
+import { Plural, Trans, useLingui } from "@lingui/react/macro";
...
-  } else if (typingUsers.length > 3) {
-    message = t`${typingUsers[0].username}, ${typingUsers[1].username}, ${typingUsers[2].username} and ${typingUsers.length - 3} others are typing...`;
-  }
+  } else if (typingUsers.length > 3) {
+    const others = typingUsers.length - 3;
+    // Render via JSX so <Plural> can pick the correct CLDR form per locale.
+    return (
+      <div className="h-5 ml-5 text-sm italic">
+        <Trans>
+          {typingUsers[0].username}, {typingUsers[1].username}, {typingUsers[2].username} and{" "}
+          <Plural value={others} one="# other is typing..." other="# others are typing..." />
+        </Trans>
+      </div>
+    );
+  }

As per coding guidelines: "Wrap all user-visible text with LinguiJS macros."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/ChatArea.tsx` around lines 98 - 107, The typing
indicator builds a localized string in ChatArea using a hard-coded English
list/grammar via the t`` template (see variable message and the typingUsers
branches), which produces wrong plural forms like "1 others" and prevents proper
localization; replace the >3 branch (and ideally the whole switch) to use
Lingui's plural (and/or select) macros so the trailing count is localized (e.g.,
plural(typingUsers.length - 3, { one: "1 other", other: "# others" })) and allow
translators to control list conjunctions/order; ensure all user-visible strings
are wrapped with Lingui macros rather than plain template literals referencing
typingUsers usernames.

Comment on lines +852 to +856
<Plural
value={extraKnownEntries.length}
one="Show 1 more item"
other={`Show ${extraKnownEntries.length} more items`}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Use # placeholder instead of template literal interpolation in <Plural>.

The other prop embeds ${extraKnownEntries.length} directly, which breaks Lingui's message extraction: each different count generates a unique message ID ("Show 2 more items", "Show 3 more items", etc.), preventing translators from providing a single template with proper plural rules. Lingui's # placeholder is replaced at render time with the value prop.

🐛 Proposed fix
                           <Plural
                             value={extraKnownEntries.length}
-                            one="Show 1 more item"
-                            other={`Show ${extraKnownEntries.length} more items`}
+                            one="Show # more item"
+                            other="Show # more items"
                           />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/message/MessageItem.tsx` around lines 852 - 856, The Plural
usage in MessageItem's JSX uses a template literal for the other prop which
generates distinct message IDs per count; change the other prop to a single
string using Lingui's '#' placeholder (e.g., "Show # more items") and keep
value={extraKnownEntries.length} (leave the one="Show 1 more item" as-is) so the
runtime substitutes the count without creating multiple message IDs.

Comment thread src/lib/eventGrouping.ts
Comment on lines +37 to +41
if (reconnectCount > 0)
return plural(reconnectCount, {
one: "reconnected",
other: `reconnected ${reconnectCount} times`,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Use # placeholder instead of template literal interpolation in plural().

The other value embeds ${reconnectCount} directly, creating a unique message ID for every count ("reconnected 2 times", "reconnected 3 times", etc.). This breaks Lingui's extraction and prevents translators from providing proper CLDR plural forms. Use # as the placeholder—Lingui substitutes it at runtime with the count argument.

🐛 Proposed fix
     return plural(reconnectCount, {
       one: "reconnected",
-      other: `reconnected ${reconnectCount} times`,
+      other: "reconnected # times",
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (reconnectCount > 0)
return plural(reconnectCount, {
one: "reconnected",
other: `reconnected ${reconnectCount} times`,
});
if (reconnectCount > 0)
return plural(reconnectCount, {
one: "reconnected",
other: "reconnected # times",
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/eventGrouping.ts` around lines 37 - 41, The pluralization call around
reconnectCount is using a template literal which embeds the number into the
message ID; change the `other` branch to use the `#` placeholder (e.g.
"reconnected # times") and ensure you pass reconnectCount as the count argument
to `plural()` so Lingui can extract proper CLDR forms; update the call that
currently references `reconnectCount` and the message strings "reconnected" /
"reconnected # times" accordingly.

Comment thread src/lib/eventGrouping.ts
Comment on lines +44 to +48
if (joinCount > 0)
return plural(joinCount, {
one: "joined",
other: `joined ${joinCount} times`,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Use # placeholder instead of template literal interpolation in plural().

Same issue as the reconnect count above: ${joinCount} in the other value creates countless distinct message IDs. Replace with # so Lingui extracts one template and translators can provide proper plural forms for all locales.

🐛 Proposed fix
     return plural(joinCount, {
       one: "joined",
-      other: `joined ${joinCount} times`,
+      other: "joined # times",
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/eventGrouping.ts` around lines 44 - 48, The plural call in
eventGrouping.ts uses a template literal (`joined ${joinCount} times`) which
prevents Lingui from extracting a single translation key; change the
`plural(joinCount, { one: "joined", other: ... })` usage to use the `#`
placeholder (e.g. `other: "joined # times"`) so the call `plural(joinCount,
...)` with `joinCount` as the count generates one extractable message ID; update
the `plural` invocation that references `joinCount` accordingly.

Comment thread src/lib/eventGrouping.ts
Comment on lines +196 to +198
.map(
([u, c]) => `${u}: ${plural(c, { one: "1 time", other: `${c} times` })}`,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Use # placeholder instead of template literal interpolation in plural().

The other value in the plural() call embeds ${c} directly, generating a separate message ID for every count. Replace with # to fix extraction and enable proper translations. (The outer ${u} username interpolation is fine—only the count substitution inside plural() needs the placeholder.)

🐛 Proposed fix
     .map(
-      ([u, c]) => `${u}: ${plural(c, { one: "1 time", other: `${c} times` })}`,
+      ([u, c]) => `${u}: ${plural(c, { one: "# time", other: "# times" })}`,
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.map(
([u, c]) => `${u}: ${plural(c, { one: "1 time", other: `${c} times` })}`,
)
.map(
([u, c]) => `${u}: ${plural(c, { one: "# time", other: "# times" })}`,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/eventGrouping.ts` around lines 196 - 198, The pluralization call
inside the map callback ([u, c]) => `${u}: ${plural(c, { one: "1 time", other:
`${c} times` })}` creates unique message IDs because it injects the count into
the string; update the plural() usage to use the '#' placeholder for the numeric
substitution (e.g., plural(c, { one: "1 time", other: "# times" })) while
keeping the outer `${u}: ` interpolation intact so translations extract a stable
message id and the count is substituted at runtime.

Comment thread src/lib/irc/IRCClient.ts
Comment on lines +857 to +859
const message =
quitMessage ||
i18n._({ id: "ObsidianIRC - Bringing IRC to the future" });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use nullish coalescing for fallback to avoid overriding explicit empty values.

|| will replace an intentional empty quitMessage with the default text. Use ?? so only null/undefined trigger the fallback.

Proposed fix
-      const message =
-        quitMessage ||
-        i18n._({ id: "ObsidianIRC - Bringing IRC to the future" });
+      const message =
+        quitMessage ??
+        i18n._({ id: "ObsidianIRC - Bringing IRC to the future" });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/irc/IRCClient.ts` around lines 857 - 859, Replace the logical-OR
fallback when building the quit message so explicit empty strings are preserved:
in IRCClient.ts update the assignment that uses quitMessage and i18n._ (the
const message currently using "quitMessage || i18n._(...)") to use the nullish
coalescing operator (??) instead, i.e., fall back to i18n._ only when
quitMessage is null or undefined; keep the same i18n._ call and variable names
(quitMessage, message) unchanged otherwise.

@@ -0,0 +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\"],\"/6BzZF\":[\"Ledenlijst aan/uit\"],\"/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\"],\"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\"],\"8ZsakT\":[\"Wachtwoord\"],\"8kR84m\":[\"Je staat op het punt een externe link te openen:\"],\"8lCgih\":[\"Regel verwijderen\"],\"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\"],\"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\"],\"BZz3md\":[\"Je persoonlijke website\"],\"Bgm/H7\":[\"Meerdere tekstregels invoeren toestaan\"],\"BiQIl1\":[\"Dit privéberichtgesprek vastmaken\"],\"BlNZZ2\":[\"Klik om naar bericht te springen\"],\"Bowq3c\":[\"Alleen operators kunnen het kanaalonderwerp wijzigen\"],\"Btozzp\":[\"Deze afbeelding is verlopen\"],\"Bycfjm\":[\"Totaal: \",[\"0\"]],\"C6IBQc\":[\"Kopieer volledige JSON\"],\"C9L9wL\":[\"Gegevensverzameling\"],\"CDq4wC\":[\"Gebruiker modereren\"],\"CHVRxG\":[\"Bericht aan @\",[\"0\"],\" (Shift+Enter voor nieuwe regel)\"],\"CN9zdR\":[\"Oper-naam en wachtwoord zijn vereist\"],\"CW3sYa\":[\"Reactie toevoegen \",[\"emoji\"]],\"CaAkqd\":[\"Afmeldingen weergeven\"],\"CbvaYj\":[\"Bannen via nickname\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Selecteer een kanaal\"],\"CsekCi\":[\"Normaal\"],\"D+NlUC\":[\"Systeem\"],\"D28t6+\":[\"is toegetreden en vertrokken\"],\"DB8zMK\":[\"Toepassen\"],\"DBcWHr\":[\"Aangepast meldingsgeluidsbestand\"],\"DTy9Xw\":[\"Mediavoorbeeldweergaven\"],\"Dj4pSr\":[\"Kies een veilig wachtwoord\"],\"Du+zn+\":[\"Zoeken...\"],\"Du2T2f\":[\"Instelling niet gevonden\"],\"DwsSVQ\":[\"Filters toepassen en vernieuwen\"],\"E3W/zd\":[\"Standaard nickname\"],\"E6nRW7\":[\"URL kopiëren\"],\"E703RG\":[\"Modi:\"],\"EAeu1Z\":[\"Uitnodiging verzenden\"],\"EFKJQT\":[\"Instelling\"],\"EGPQBv\":[\"Aangepaste floodregels (+f)\"],\"ELik0r\":[\"Volledig privacybeleid bekijken\"],\"EPbeC2\":[\"Kanaalonderwerp bekijken of bewerken\"],\"EQCDNT\":[\"Voer oper-gebruikersnaam in...\"],\"EUvulZ\":[\"1 bericht gevonden dat overeenkomt met \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Volgende afbeelding\"],\"EdQY6l\":[\"Geen\"],\"EnqLYU\":[\"Servers zoeken...\"],\"F0OKMc\":[\"Server bewerken\"],\"F6Int2\":[\"Markeringen inschakelen\"],\"FDoLyE\":[\"Max. gebruikers\"],\"FUU/hZ\":[\"Bepaalt hoeveel externe media in de chat worden geladen.\"],\"Fdp03t\":[\"aan\"],\"FfPWR0\":[\"Venster\"],\"FjkaiT\":[\"Uitzoomen\"],\"FlqOE9\":[\"Wat dit betekent:\"],\"FolHNl\":[\"Je account en authenticatie beheren\"],\"Fp2Dif\":[\"De server verlaten\"],\"G5KmCc\":[\"GZ-Line (globale Z-Line)\"],\"GDs0lz\":[\"<0>Risico:</0> Gevoelige informatie (berichten, privégesprekken, authenticatiegegevens) kan worden blootgesteld aan netwerkbeheerders of aanvallers tussen IRC-servers.\"],\"GR+2I3\":[\"Uitnodigingsmasker toevoegen (bijv. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Losgekoppelde serverberichten sluiten\"],\"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\"],\"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:\"],\"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\"],\"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*\"],\"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\"]}]],\"Weq9zb\":[\"Algemeen\"],\"Wfj7Sk\":[\"Meldingsgeluiden dempen of activeren\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Gebruikersprofiel\"],\"X6S3lt\":[\"Instellingen, kanalen, servers zoeken...\"],\"XEHan5\":[\"Toch doorgaan\"],\"XI1+wb\":[\"Ongeldig formaat\"],\"XIXeuC\":[\"Bericht aan @\",[\"0\"]],\"XMS+k4\":[\"Privébericht starten\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Privégesprek losmaken\"],\"Xm/s+u\":[\"Weergave\"],\"Xp2n93\":[\"Toont media van de vertrouwde bestandshost van je server. Er worden geen verzoeken gedaan aan externe diensten.\"],\"XvjC4F\":[\"Opslaan...\"],\"Y/qryO\":[\"Geen gebruikers gevonden die overeenkomen met je zoekopdracht\"],\"YAqRpI\":[\"Accountregistratie voor \",[\"account\"],\" geslaagd: \",[\"message\"]],\"YEfzvP\":[\"Beveiligd onderwerp (+t)\"],\"YQOn6a\":[\"Ledenlijst inklappen\"],\"YRCoE9\":[\"Kanaaloperator\"],\"YURQaF\":[\"Profiel bekijken\"],\"YdBSvr\":[\"Mediaweergave en externe inhoud beheren\"],\"Yj6U3V\":[\"Geen centrale server:\"],\"YjvpGx\":[\"Voornaamwoorden\"],\"YqH4l4\":[\"Geen sleutel\"],\"YyUPpV\":[\"Account:\"],\"ZJSWfw\":[\"Bericht dat wordt weergegeven wanneer je de verbinding met de server verbreekt\"],\"ZR1dJ4\":[\"Uitnodigingen\"],\"ZdWg0V\":[\"Openen in browser\"],\"ZhRBbl\":[\"Berichten zoeken…\"],\"Zmcu3y\":[\"Geavanceerde filters\"],\"a2/8e5\":[\"Onderwerp ingesteld na (min geleden)\"],\"aHKcKc\":[\"Vorige pagina\"],\"aJTbXX\":[\"Oper-wachtwoord\"],\"aQryQv\":[\"Patroon bestaat al\"],\"aW9pLN\":[\"Maximaal aantal toegestane gebruikers in het kanaal. Laat leeg voor geen limiet.\"],\"ah4fmZ\":[\"Toont ook voorbeeldweergaven van YouTube, Vimeo, SoundCloud en vergelijkbare bekende diensten.\"],\"aifXak\":[\"Geen media in dit kanaal\"],\"ap2zBz\":[\"Ontspannen\"],\"az8lvo\":[\"Uit\"],\"azXSNo\":[\"Ledenlijst uitvouwen\"],\"azdliB\":[\"Aanmelden bij een account\"],\"b26wlF\":[\"zij/haar\"],\"bD/+Ei\":[\"Streng\"],\"bQ6BJn\":[\"Stel gedetailleerde floodbeveiligingsregels in. Elke regel bepaalt welk type activiteit wordt bewaakt en welke actie wordt ondernomen als drempelwaarden worden overschreden.\"],\"beV7+y\":[\"De gebruiker ontvangt een uitnodiging om deel te nemen aan \",[\"channelName\"],\".\"],\"bk84cH\":[\"Afwezigheidsbericht\"],\"bkHdLj\":[\"IRC-server toevoegen\"],\"bmQLn5\":[\"Regel toevoegen\"],\"bwRvnp\":[\"Actie\"],\"c8+EVZ\":[\"Geverifieerd account\"],\"cGYUlD\":[\"Er worden geen mediavoorbeeldweergaven geladen.\"],\"cLF98o\":[\"Reacties tonen (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Geen gebruikers beschikbaar\"],\"cSgpoS\":[\"Privégesprek vastmaken\"],\"cde3ce\":[\"Bericht aan <0>\",[\"0\"],\"</0>\"],\"chQsxg\":[\"Kopieer opgemaakte uitvoer\"],\"cl/A5J\":[\"Welkom bij \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Verwijderen\"],\"coPLXT\":[\"We slaan je IRC-communicatie niet op onze servers op\"],\"crYH/6\":[\"SoundCloud-speler\"],\"d3sis4\":[\"Server toevoegen\"],\"d9aN5k\":[[\"username\"],\" uit het kanaal verwijderen\"],\"dEgA5A\":[\"Annuleren\"],\"dGi1We\":[\"Dit privéberichtgesprek losmaken\"],\"dJVuyC\":[\"heeft \",[\"channelName\"],\" verlaten (\",[\"reason\"],\")\"],\"dMtLDE\":[\"aan\"],\"dXqxlh\":[\"<0>⚠️ Beveiligingsrisico!</0> Deze verbinding kan kwetsbaar zijn voor onderschepping of man-in-the-middle-aanvallen.\"],\"da9Q/R\":[\"Kanaalmodi gewijzigd\"],\"dhJN3N\":[\"Reacties tonen\"],\"dj2xTE\":[\"Melding sluiten\"],\"dpCzmC\":[\"Floodbeveiligingsinstellingen\"],\"e9dQpT\":[\"Wil je deze link in een nieuw tabblad openen?\"],\"ePK91l\":[\"Bewerken\"],\"eYBDuB\":[\"Upload een afbeelding of geef een URL op met optionele \",[\"size\"],\"-vervanging voor dynamische grootte\"],\"edBbee\":[[\"username\"],\" bannen via hostmasker (voorkomt dat ze opnieuw deelnemen via hetzelfde IP/host)\"],\"ekfzWq\":[\"Gebruikersinstellingen\"],\"elPDWs\":[\"Pas je IRC-clientervaring aan\"],\"eu2osY\":[\"<0>💡 Aanbeveling:</0> Ga alleen verder als je deze server vertrouwt en de risico's begrijpt. Deel geen gevoelige informatie of wachtwoorden via deze verbinding.\"],\"euEhbr\":[\"Klik om deel te nemen aan \",[\"channel\"]],\"ez3vLd\":[\"Meerdere regels invoer inschakelen\"],\"f0J5Ki\":[\"Server-naar-servercommunicatie kan niet-versleutelde verbindingen gebruiken\"],\"f9BHJk\":[\"Gebruiker waarschuwen\"],\"fDOLLd\":[\"Geen kanalen gevonden.\"],\"ffzDkB\":[\"Anonieme analyses:\"],\"fq1GF9\":[\"Weergeven wanneer gebruikers de verbinding met de server verbreken\"],\"gEF57C\":[\"Deze server ondersteunt slechts één verbindingstype\"],\"gJuLUI\":[\"Negeerlijst\"],\"gNzMrk\":[\"Huidige avatar\"],\"gjPWyO\":[\"Voer bijnaam in...\"],\"gz6UQ3\":[\"Maximaliseren\"],\"h6razj\":[\"Kanaalnaammasker uitsluiten\"],\"hG6jnw\":[\"Geen onderwerp ingesteld\"],\"hG89Ed\":[\"Afbeelding\"],\"hZ6znB\":[\"Poort\"],\"ha+Bz5\":[\"bijv. 100:1440\"],\"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\"]}]],\"l5jmzx\":[[\"0\"],\" en \",[\"1\"],\" zijn aan het typen...\"],\"lHy8N5\":[\"Meer kanalen laden...\"],\"lbpf14\":[\"Deelnemen aan \",[\"value\"]],\"lfFsZ4\":[\"Kanalen\"],\"lkNdiH\":[\"Accountnaam\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Afbeelding uploaden\"],\"loQxaJ\":[\"Ik ben terug\"],\"lvfaxv\":[\"START\"],\"m16xKo\":[\"Toevoegen\"],\"m8flAk\":[\"Voorbeeld (nog niet geüpload)\"],\"mEPxTp\":[\"<0>⚠️ Wees voorzichtig!</0> Open alleen links van vertrouwde bronnen. Kwaadaardige links kunnen je beveiliging of privacy in gevaar brengen.\"],\"mHGdhG\":[\"Serverinformatie\"],\"mHS8lb\":[\"Bericht in #\",[\"0\"]],\"mMYBD9\":[\"Breed — Ruimere beveiligingsscope\"],\"mTGsPd\":[\"Kanaalonderwerp\"],\"mU8j6O\":[\"Geen externe berichten (+n)\"],\"mZp8FL\":[\"Automatisch terugvallen op één regel\"],\"mdQu8G\":[\"JouwNickname\"],\"miSSBQ\":[\"Reacties (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Gebruiker is geverifieerd\"],\"mwtcGl\":[\"Reacties sluiten\"],\"mzI/c+\":[\"Downloaden\"],\"n3fGRk\":[\"ingesteld door \",[\"0\"]],\"nE9jsU\":[\"Ontspannen — Minder agressieve beveiliging\"],\"nNflMD\":[\"Kanaal verlaten\"],\"nPXkBi\":[\"WHOIS-gegevens laden...\"],\"nQnxxF\":[\"Bericht in #\",[\"0\"],\" (Shift+Enter voor nieuwe regel)\"],\"nWMRxa\":[\"Losmaken\"],\"nkC032\":[\"Geen floodprofiel\"],\"o69z4d\":[\"Een waarschuwingsbericht sturen naar \",[\"username\"]],\"o9ylQi\":[\"Zoek naar GIF's om te beginnen\"],\"oFGkER\":[\"Serverberichten\"],\"oOi11l\":[\"Naar beneden scrollen\"],\"oQEzQR\":[\"Nieuw DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Man-in-the-middle-aanvallen op serververbindingen zijn mogelijk\"],\"oeqmmJ\":[\"Vertrouwde bronnen\"],\"ovBPCi\":[\"Standaard\"],\"p0Z69r\":[\"Patroon kan niet leeg zijn\"],\"p1KgtK\":[\"Audio laden mislukt\"],\"p59pEv\":[\"Extra details\"],\"p7sRI6\":[\"Laat anderen weten wanneer je typt\"],\"pBm1od\":[\"Geheim kanaal\"],\"pNmiXx\":[\"Je standaard nickname voor alle servers\"],\"pUUo9G\":[\"Hostnaam:\"],\"pVGPmz\":[\"Accountwachtwoord\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"Geen gegevens\"],\"pm6+q5\":[\"Beveiligingswaarschuwing\"],\"pn5qSs\":[\"Aanvullende informatie\"],\"q0cR4S\":[\"is nu bekend als **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanaal verschijnt niet in LIST- of NAMES-opdrachten\"],\"qLpTm/\":[\"Reactie \",[\"emoji\"],\" verwijderen\"],\"qVkGWK\":[\"Vastmaken\"],\"qY8wNa\":[\"Startpagina\"],\"qb0xJ7\":[\"Gebruik jokertekens: * komt overeen met een reeks, ? met één teken. Voorbeelden: nick!*@*, *!*@host.com, *!*gebruiker@*\"],\"qhzpRq\":[\"Kanaalsleutel (+k)\"],\"qtoOYG\":[\"Geen limiet\"],\"r1W2AS\":[\"Afbeelding van bestandshost\"],\"rIPR2O\":[\"Onderwerp ingesteld voor (min geleden)\"],\"rMMSYo\":[\"Maximale lengte is \",[\"0\"]],\"rWtzQe\":[\"Het netwerk splitste en herverbond. ✅\"],\"rYG2u6\":[\"Even wachten...\"],\"rdUucN\":[\"Voorbeeld\"],\"rjGI/Q\":[\"Privacy\"],\"rk8iDX\":[\"GIF's laden...\"],\"rn6SBY\":[\"Dempen opheffen\"],\"s/UKqq\":[\"Uit het kanaal verwijderd\"],\"s8cATI\":[\"heeft \",[\"channelName\"],\" betreden\"],\"sCO9ue\":[\"De verbinding met <0>\",[\"serverName\"],\"</0> heeft de volgende beveiligingsproblemen:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"is nu bekend als **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" heeft je uitgenodigd om deel te nemen aan \",[\"channel\"]],\"sby+1/\":[\"Klik om te kopiëren\"],\"sfN25C\":[\"Je echte of volledige naam\"],\"sliuzR\":[\"Link openen\"],\"sqrO9R\":[\"Aangepaste vermeldingen\"],\"sr6RdJ\":[\"Meerdere regels met Shift+Enter\"],\"swrCpB\":[\"Het kanaal is hernoemd van \",[\"oldName\"],\" naar \",[\"newName\"],\" door \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Geavanceerd\"],\"t/YqKh\":[\"Verwijderen\"],\"t47eHD\":[\"Je unieke identificatie op deze server\"],\"tAkAh0\":[\"URL met optionele \",[\"size\"],\"-vervanging voor dynamische grootte. Voorbeeld: https://voorbeeld.com/avatar/\",[\"size\"],\"/kanaal.jpg\"],\"tXLJS3\":[\"Zijbalk met kanaallijst weergeven of verbergen\"],\"tfDRzk\":[\"Opslaan\"],\"tiBsJk\":[\"heeft \",[\"channelName\"],\" verlaten\"],\"tt4/UD\":[\"verliet de server (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nickname {nick} is al in gebruik, probeer opnieuw met {newNick}\"],\"u0a8B4\":[\"Authenticeren als IRC Operator voor beheerderstoegang\"],\"u0rWFU\":[\"Aangemaakt na (min geleden)\"],\"u72w3t\":[\"Gebruikers en patronen om te negeren\"],\"u7jc2L\":[\"verliet de server\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Opslaan mislukt: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-servers:\"],\"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\"],\"xCJdfg\":[\"Wissen\"],\"xUHRTR\":[\"Automatisch als operator authenticeren bij verbinden\"],\"xWHwwQ\":[\"Bannen\"],\"xYilR2\":[\"Media\"],\"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...\"],\"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\"]],\"zebeLu\":[\"Oper-gebruikersnaam invoeren\"],\"zpr0Bw\":[\"GZ-Line\"]}"); No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix user-facing Dutch typos in catalog strings.

On Line 1, I spotted at least two visible typos: Kanaelconfiguratie-instellingen openen (PPqfdA) and floodbeveililingsprofiel (SkZcl+). Please correct in source translations and recompile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/nl/messages.mjs` at line 1, Fix the Dutch typos in the
translation catalog by updating the strings for keys "PPqfdA" (currently
"Kanaelconfiguratie-instellingen openen" -> correct to
"Kanaalconfiguratie-instellingen openen") and "SkZcl+" (currently
"floodbeveililingsprofiel" -> correct to "floodbeveiligingsprofiel") in the
source translation file or upstream translation source, then rebuild/recompile
the messages bundle (the code that generates messages.mjs / JSON.parse output)
so the corrected translations are emitted into the exported messages.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Extract and parse the JSON to examine the specific message entries

# First, let's extract the JSON from the minified string
python3 << 'EOF'
import json
import re

# Read the file
with open('src/locales/nl/messages.mjs', 'r') as f:
    content = f.read()

# Extract JSON string from JSON.parse()
match = re.search(r'JSON\.parse\("(.+)"\)', content)
if match:
    json_str = match.group(1)
    # Unescape the JSON string
    try:
        data = json.loads(f'"{json_str}"')
        messages = json.loads(data)
        
        # Extract specific entries
        print("=== swrCpB entry ===")
        if 'swrCpB' in messages:
            print(json.dumps(messages['swrCpB'], ensure_ascii=False, indent=2))
        else:
            print("swrCpB not found")
        
        print("\n=== PPqfdA entry ===")
        if 'PPqfdA' in messages:
            print(json.dumps(messages['PPqfdA'], ensure_ascii=False, indent=2))
        else:
            print("PPqfdA not found")
        
        print("\n=== SkZcl+ entry ===")
        if 'SkZcl+' in messages:
            print(json.dumps(messages['SkZcl+'], ensure_ascii=False, indent=2))
        else:
            print("SkZcl+ not found")
            
    except json.JSONDecodeError as e:
        print(f"JSON decode error: {e}")
else:
    print("Could not find JSON.parse pattern")
EOF

Repository: ObsidianIRC/ObsidianIRC

Length of output: 475


🏁 Script executed:

# Let's also check if there's a source message file or extract configuration
find . -type f -name "*.json" -o -name "messages.pot" -o -name "*.po" | head -20

Repository: ObsidianIRC/ObsidianIRC

Length of output: 743


🏁 Script executed:

# Check the project structure for i18n related files and configuration
fd -t f '(lingui|i18n)' --max-depth 3

Repository: ObsidianIRC/ObsidianIRC

Length of output: 112


🏁 Script executed:

# Check if the Dutch .po file exists and examine the source entries
if [ -f "src/locales/nl/messages.po" ]; then
  echo "=== Examining nl/messages.po ==="
  # Extract the swrCpB entry
  grep -A 20 '"swrCpB"' src/locales/nl/messages.po
  echo ""
  echo "=== Examining PPqfdA in .po ==="
  grep -A 5 '"PPqfdA"' src/locales/nl/messages.po
  echo ""
  echo "=== Examining SkZcl+ in .po ==="
  grep -A 5 '"SkZcl+"' src/locales/nl/messages.po
else
  echo "nl/messages.po does not exist"
  ls -la src/locales/
fi

Repository: ObsidianIRC/ObsidianIRC

Length of output: 166


🏁 Script executed:

# Check the lingui config to understand the workflow
cat lingui.config.ts

Repository: ObsidianIRC/ObsidianIRC

Length of output: 604


🏁 Script executed:

# Check what .po files exist
ls -la src/locales/*/messages.po | head -20

Repository: ObsidianIRC/ObsidianIRC

Length of output: 1552


🏁 Script executed:

# Check the English .po file to see if it has the swrCpB entry and understand the format
grep -A 10 'msgctxt "swrCpB"' src/locales/en/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

# Let's check if there's a Dutch .po file with different naming
find src/locales -type f -name "*nl*" -o -name "*dutch*"

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

# Check the actual content of the locales directory
ls -la src/locales/

Repository: ObsidianIRC/ObsidianIRC

Length of output: 1231


🏁 Script executed:

# Now check the Dutch .po file for the swrCpB entry
grep -A 15 'msgctxt "swrCpB"' src/locales/nl/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

# Check PPqfdA and SkZcl+ in Dutch .po
grep -A 5 'msgctxt "PPqfdA"' src/locales/nl/messages.po
echo "---"
grep -A 5 'msgctxt "SkZcl+"' src/locales/nl/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 71


🏁 Script executed:

# Also check the English source for swrCpB to understand the proper placeholder structure
grep -B 2 -A 15 'msgctxt "swrCpB"' src/locales/en/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

# Let's look at the actual structure of the .po files first
head -50 src/locales/nl/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 41027


🏁 Script executed:

# Search for swrCpB differently - it might be encoded or formatted differently
grep -i "swrCpB" src/locales/nl/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

# Search for the Dutch text we know exists in the compiled version
grep -i "kanaal is hernoemd" src/locales/nl/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 143


🏁 Script executed:

# Check the structure of the English .po file
head -100 src/locales/en/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 42703


🏁 Script executed:

# Look for swrCpB in the Dutch .po file more carefully
grep -B 5 -A 2 "{user}{0}" src/locales/nl/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 404


🏁 Script executed:

# Check if the trailing {0} is actually used in the source code or if it's unintended
grep -r "swrCpB" src --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx"

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

# Let's also verify the exact source entries for PPqfdA and SkZcl+ in the Dutch .po
grep -B 2 "Kanaelconfiguratie" src/locales/nl/messages.po
echo "---"
grep -B 2 "floodbeveililingsprofiel" src/locales/nl/messages.po

Repository: ObsidianIRC/ObsidianIRC

Length of output: 545


Fix Dutch spelling errors in the catalog.

Two Dutch translation entries contain typos:

  • PPqfdA: "Kanaelconfiguratie" should be "Kanaalconfiguratie"
  • SkZcl+: "floodbeveililingsprofiel" should be "floodbeveiliging"

Correct these in the source src/locales/nl/messages.po and recompile using npm run i18n:compile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/nl/messages.mjs` at line 1, Update the Dutch catalog entries for
the two keys with typos: change the value for key "PPqfdA" from
"Kanaelconfiguratie" to "Kanaalconfiguratie" and change the value for key
"SkZcl+" from "floodbeveililingsprofiel" to "floodbeveiliging" in the source
messages (messages.po), then recompile translations by running the i18n compile
script (npm run i18n:compile).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants