From ecdead8278b1508869261db4ebe414f183d55683 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 20 Jul 2026 02:03:05 +0000 Subject: [PATCH] Pluralize count-dependent messages with a CLDR one/other selector and gate catalog consistency with a test --- api/src/lib.rs | 60 +++++++++++ lang/de.json | 77 +++++++++----- lang/es-ar.json | 77 +++++++++----- lang/es.json | 77 +++++++++----- lang/fr.json | 77 +++++++++----- lang/pt-br.json | 77 +++++++++----- lang/pt.json | 77 +++++++++----- modules/botserv/src/badwords.rs | 2 +- modules/botserv/src/bot.rs | 2 +- modules/botserv/src/info.rs | 2 +- modules/botserv/src/kick.rs | 2 +- modules/botserv/src/set.rs | 2 +- modules/botserv/src/trigger.rs | 2 +- modules/chanfix/src/fix.rs | 2 +- modules/chanfix/src/scores.rs | 2 +- modules/chanserv/src/akick.rs | 2 +- modules/chanserv/src/flags.rs | 2 +- modules/groupserv/src/list.rs | 2 +- modules/helpserv/src/list.rs | 2 +- modules/memoserv/src/del.rs | 2 +- modules/memoserv/src/info.rs | 4 +- modules/memoserv/src/sendall.rs | 4 +- modules/memoserv/src/staff.rs | 4 +- modules/nickserv/src/info.rs | 2 +- modules/nickserv/src/list.rs | 2 +- modules/nickserv/src/update.rs | 3 +- modules/operserv/src/chankill.rs | 2 +- modules/operserv/src/notify.rs | 2 +- modules/operserv/src/session.rs | 4 +- modules/reportserv/src/list.rs | 2 +- modules/statserv/src/channel.rs | 2 +- src/engine/tests.rs | 2 +- src/i18n_check.rs | 175 +++++++++++++++++++++++++++++++ src/main.rs | 2 + 34 files changed, 572 insertions(+), 186 deletions(-) create mode 100644 src/i18n_check.rs diff --git a/api/src/lib.rs b/api/src/lib.rs index a7dc93b..55d3195 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -520,6 +520,46 @@ macro_rules! t { }; } +/// CLDR cardinal plural category. `one` vs `other` covers every language echo +/// ships: en/de/es (and es-ar)/pt use `one` only for n==1, while fr and pt-br +/// count 0 as singular too. A language with richer rules (Slavic `few`/`many`, +/// Arabic) would need a real rule table here — and, because the catalog is keyed +/// by the English text, English only offers one/other forms to key off, so such +/// a language can't gain extra categories without a different catalog shape. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Plural { + One, + Other, +} + +/// Pick the plural category for `n` under `lang`'s rule. +pub fn plural_category(lang: &str, n: u64) -> Plural { + let is_one = match lang { + "fr" | "pt-br" => n == 0 || n == 1, + _ => n == 1, + }; + if is_one { + Plural::One + } else { + Plural::Other + } +} + +/// Localize a count-dependent reply. Selects the `one` or `other` English +/// template by the target language's plural rule, then renders it like [`t!`]. +/// Both forms are message ids, so both need a catalog entry in each language. +/// `plural!(ctx, n, one = "…{n} entry…", other = "…{n} entries…", n = n, …)`. +#[macro_export] +macro_rules! plural { + ($ctx:expr, $n:expr, one = $one:literal, other = $other:literal $(, $name:ident = $val:expr)* $(,)?) => {{ + let __key = match $crate::plural_category($ctx.lang(), ($n) as u64) { + $crate::Plural::One => $one, + $crate::Plural::Other => $other, + }; + $crate::render($ctx.lang(), __key, &[$((stringify!($name), format!("{}", $val))),*]) + }}; +} + /// The intent sink a service writes to. A service never mutates the network or /// the store itself; it pushes normalized actions (via [`ServiceCtx::notice`] /// and friends) that the engine drains and applies. @@ -2887,6 +2927,26 @@ mod help_tests { assert_eq!(render("fr", "not translated", &[]), "not translated"); } + #[test] + fn plural_rules_match_cldr() { + use Plural::{One, Other}; + // Most languages: singular only for exactly 1. + for lang in ["en", "de", "es", "es-ar", "pt"] { + assert_eq!(plural_category(lang, 1), One, "{lang} 1"); + assert_eq!(plural_category(lang, 0), Other, "{lang} 0"); + assert_eq!(plural_category(lang, 2), Other, "{lang} 2"); + } + // French and Brazilian Portuguese count 0 as singular. + for lang in ["fr", "pt-br"] { + assert_eq!(plural_category(lang, 0), One, "{lang} 0"); + assert_eq!(plural_category(lang, 1), One, "{lang} 1"); + assert_eq!(plural_category(lang, 2), Other, "{lang} 2"); + } + // Unknown language falls back to the n==1 rule. + assert_eq!(plural_category("xx", 1), One); + assert_eq!(plural_category("xx", 5), Other); + } + #[test] fn valid_email_rejects_header_injection() { assert!(valid_email("alice@example.com")); diff --git a/lang/de.json b/lang/de.json index ff3374d..87b750e 100644 --- a/lang/de.json +++ b/lang/de.json @@ -61,7 +61,6 @@ "\u0002{chan}\u0002 isn't suspended.": "\u0002{chan}\u0002 ist nicht gesperrt.", "\u0002{chan}\u0002 was already set that way.": "\u0002{chan}\u0002 war bereits so eingestellt.", "\u0002{chan}\u0002 will no longer expire.": "\u0002{chan}\u0002 wird nicht mehr ablaufen.", - "\u0002{chan}\u0002 — \u0002{lines}\u0002 line(s) seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 Zeile(n) gesehen.", "\u0002{code}\u0002 isn't an available language. Available: {list}.": "\u0002{code}\u0002 ist keine verfügbare Sprache. Verfügbar: {list}.", "\u0002{d}\u0002 isn't a valid expiry — try 30d, 12h, 45m, or 0.": "\u0002{d}\u0002 ist kein gültiges Ablaufdatum — versuche 30d, 12h, 45m oder 0.", "\u0002{host}\u0002 is already in use. Please choose another.": "\u0002{host}\u0002 wird bereits verwendet. Bitte wähle einen anderen.", @@ -69,7 +68,6 @@ "\u0002{host}\u0002 isn't a valid host (letters, digits, hyphens and dots).": "\u0002{host}\u0002 ist kein gültiger Host (Buchstaben, Ziffern, Bindestriche und Punkte).", "\u0002{host}\u0002 isn't a valid host.": "\u0002{host}\u0002 ist kein gültiger Host.", "\u0002{host}\u0002 isn't allowed here. Please choose another.": "\u0002{host}\u0002 ist hier nicht erlaubt. Bitte wähle einen anderen.", - "\u0002{ip}\u0002 has \u0002{count}\u0002 live session(s).": "\u0002{ip}\u0002 hat \u0002{count}\u0002 aktive Sitzung(en).", "\u0002{label}\u0002 access on \u0002{chan}\u0002: \u0002{level}\u0002": "\u0002{label}\u0002-Zugriff auf \u0002{chan}\u0002: \u0002{level}\u0002", "\u0002{mask}\u0002 isn't on \u0002{chan}\u0002's auto-kick list.": "\u0002{mask}\u0002 steht nicht auf der Auto-Kick-Liste von \u0002{chan}\u0002.", "\u0002{mask}\u0002 isn't on the ignore list.": "\u0002{mask}\u0002 steht nicht auf der Ignore-Liste.", @@ -88,7 +86,6 @@ "\u0002{nick}\u0002 isn't grouped to your account.": "\u0002{nick}\u0002 ist nicht zu deinem Konto gruppiert.", "\u0002{nick}\u0002 isn't here.": "\u0002{nick}\u0002 ist nicht hier.", "\u0002{nick}\u0002 was last seen {when} ({what}).": "\u0002{nick}\u0002 wurde zuletzt {when} gesehen ({what}).", - "\u0002{n}\u0002 vote(s) will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 Stimme(n) lösen jetzt einen \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002 aus.", "\u0002{pattern}\u0002 isn't a valid regular expression.": "\u0002{pattern}\u0002 ist kein gültiger regulärer Ausdruck.", "\u0002{raw}\u0002 isn't a valid \u0002{target}\u0002 mask.": "\u0002{raw}\u0002 ist keine gültige \u0002{target}\u0002-Maske.", "\u0002{target}\u0002 had no access to \u0002{chan}\u0002.": "\u0002{target}\u0002 hatte keinen Zugriff auf \u0002{chan}\u0002.", @@ -143,7 +140,6 @@ " (custom)": " (eigen)", " (no activity counters yet)": " (noch keine Aktivitätszähler)", " About : \u0002{target}\u0002": " Über : \u0002{target}\u0002", - " Auto-join : {count} channel(s) — see \u0002AJOIN LIST\u0002": " Auto-Join : {count} Kanal/Kanäle — siehe \u0002AJOIN LIST\u0002", " By : \u0002{by}\u0002": " Von : \u0002{by}\u0002", " Description: {desc}": " Beschreibung: {desc}", " Email : (none set)": " E-Mail : (keine gesetzt)", @@ -165,7 +161,6 @@ " Real name: {gecos}": " Echter Name: {gecos}", " Reason : {reason}": " Grund : {reason}", " Registered : {when}": " Registriert : {when}", - " Serving {count} channel(s): {list}": " Bedient {count} Kanal/Kanäle: {list}", " Staff note : {note}": " Team-Notiz : {note}", " Suspended : by \u0002{by}\u0002 — {reason}": " Gesperrt : von \u0002{by}\u0002 — {reason}", " URL : {url}": " URL : {url}", @@ -278,7 +273,6 @@ "Both channels must be registered.": "Beide Kanäle müssen registriert sein.", "Bots ({count}):": "Bots ({count}):", "CALC and show each die": "CALC und jeden Würfel anzeigen", - "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) AKILL'd.": "CHANKILL auf \u0002{chan}\u0002: \u0002{banned}\u0002 Host(s) mit AKILL belegt.", "Can't activate: \u0002{host}\u0002 is on the forbidden list.": "Kann nicht aktivieren: \u0002{host}\u0002 steht auf der Verbotsliste.", "Can't activate: {msg}": "Kann nicht aktivieren: {msg}", "Challenge \u0002#{id}\u0002 ({game}) sent to \u0002{target}\u0002.": "Herausforderung \u0002#{id}\u0002 ({game}) an \u0002{target}\u0002 gesendet.", @@ -291,10 +285,6 @@ "Channels released: {list}.": "Freigegebene Kanäle: {list}.", "Channels you have access on ({count}):": "Kanäle, auf die du Zugriff hast ({count}):", "Classement \u0002{game}\u0002 vide. Sois le premier à gagner !": "Rangliste \u0002{game}\u0002 leer. Sei der Erste, der gewinnt!", - "Cleared \u0002{count}\u0002 entr{suffix} from \u0002{chan}\u0002's auto-kick list.": "\u0002{count}\u0002 Eintr{suffix} aus der Auto-Kick-Liste von \u0002{chan}\u0002 gelöscht.", - "Cleared \u0002{n}\u0002 badword pattern(s) from \u0002{chan}\u0002.": "\u0002{n}\u0002 Badword-Muster aus \u0002{chan}\u0002 gelöscht.", - "Cleared \u0002{n}\u0002 notify watch(es).": "\u0002{n}\u0002 Notify-Überwachung(en) gelöscht.", - "Cleared \u0002{n}\u0002 trigger(s) from \u0002{chan}\u0002.": "\u0002{n}\u0002 Trigger aus \u0002{chan}\u0002 gelöscht.", "Cleared \u0002{target}\u0002's access to \u0002{chan}\u0002.": "Zugriff von \u0002{target}\u0002 auf \u0002{chan}\u0002 gelöscht.", "Cleared the *!*@{host} ban on \u0002{chan}\u0002.": "Den *!*@{host}-Ban auf \u0002{chan}\u0002 entfernt.", "Contact email for \u0002{chan}\u0002 cleared.": "Kontakt-E-Mail für \u0002{chan}\u0002 gelöscht.", @@ -306,22 +296,17 @@ "Couldn't update \u0002{chan}\u0002 — please try again in a moment.": "\u0002{chan}\u0002 konnte nicht aktualisiert werden — bitte versuche es gleich noch einmal.", "DEFCON set to \u0002{level}\u0002 — {desc}.": "DEFCON auf \u0002{level}\u0002 gesetzt — {desc}.", "Declined.": "Abgelehnt.", - "Deleted all \u0002{count}\u0002 memo(s).": "Alle \u0002{count}\u0002 Memo(s) gelöscht.", "Description for \u0002{chan}\u0002 updated.": "Beschreibung für \u0002{chan}\u0002 aktualisiert.", "DiceServ rolls dice and evaluates maths for tabletop games. Dice: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Also + - * / ^ %, parentheses, functions (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, and \u00023~2d6\u0002 to repeat.": "DiceServ würfelt und wertet Mathe für Tabletop-Spiele aus. Würfel: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Auch + - * / ^ %, Klammern, Funktionen (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002 und \u00023~2d6\u0002 zum Wiederholen.", "DictServ looks words up in the dict.org dictionaries. Try \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (thesaurus), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, and more — \u0002LOOKUP\u0002 lists them all.": "DictServ schlägt Wörter in den dict.org-Wörterbüchern nach. Versuche \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (Thesaurus), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002 und mehr — \u0002LOOKUP\u0002 listet sie alle auf.", "Email for \u0002{account}\u0002 cleared.": "E-Mail für \u0002{account}\u0002 gelöscht.", "Email for \u0002{account}\u0002 updated.": "E-Mail für \u0002{account}\u0002 aktualisiert.", "End of exception list ({count} shown).": "Ende der Ausnahmeliste ({count} angezeigt).", - "End of flags ({count} entr{suffix}).": "Ende der Flags ({count} Eintr{suffix}).", "End of ignore list ({count} shown).": "Ende der Ignore-Liste ({count} angezeigt).", "End of jupe list ({count} shown).": "Ende der Jupe-Liste ({count} angezeigt).", - "End of list ({count} group(s)).": "Ende der Liste ({count} Gruppe(n)).", - "End of list — {count} match(es){more}.": "Ende der Liste — {count} Treffer{more}.", "End of notify list ({shown} shown).": "Ende der Notify-Liste ({shown} angezeigt).", "End of results ({count} shown, newest first — refine the search to narrow).": "Ende der Ergebnisse ({count} angezeigt, neueste zuerst — verfeinere die Suche zum Eingrenzen).", "End of runtime operator list ({count} shown).": "Ende der Laufzeit-Operator-Liste ({count} angezeigt).", - "End of session list ({count} IP(s)).": "Ende der Sitzungsliste ({count} IP(s)).", "End of spam-filter list ({shown} shown).": "Ende der Spam-Filter-Liste ({shown} angezeigt).", "End of {name} list ({shown} shown).": "Ende der {name}-Liste ({shown} angezeigt).", "End of {which} bulletins ({count} shown).": "Ende der {which}-Bulletins ({count} angezeigt).", @@ -387,8 +372,6 @@ "Mask must be a \u0002#channel\u0002, a \u0002nick!user@host\u0002 pattern, or a bare nick glob — and no spaces.": "Maske muss ein \u0002#channel\u0002, ein \u0002nick!user@host\u0002-Muster oder ein reiner Nick-Glob sein — und keine Leerzeichen.", "Memo #\u0002{num}\u0002 from \u0002{from}\u0002 ({when}):": "Memo #\u0002{num}\u0002 von \u0002{from}\u0002 ({when}):", "Memo #\u0002{n}\u0002 deleted.": "Memo #\u0002{n}\u0002 gelöscht.", - "Memo sent to \u0002{sent}\u0002 account(s).": "Memo an \u0002{sent}\u0002 Konto/Konten gesendet.", - "Memo sent to \u0002{sent}\u0002 operator(s).": "Memo an \u0002{sent}\u0002 Operator(en) gesendet.", "Memo sent to \u0002{target}\u0002.": "Memo an \u0002{target}\u0002 gesendet.", "MemoServ delivers messages to registered users, online or not. You must be identified to use it.": "MemoServ stellt Nachrichten an registrierte Nutzer zu, ob online oder nicht. Du musst identifiziert sein, um es zu nutzen.", "Mode lock for \u0002{chan}\u0002 updated.": "Mode-Lock für \u0002{chan}\u0002 aktualisiert.", @@ -485,12 +468,10 @@ "Removed \u0002{mask}\u0002 from \u0002{chan}\u0002's auto-kick list.": "\u0002{mask}\u0002 wurde von der Auto-Kick-Liste von \u0002{chan}\u0002 entfernt.", "Removed \u0002{target}\u0002 from \u0002{name}\u0002.": "\u0002{target}\u0002 wurde aus \u0002{name}\u0002 entfernt.", "Removed \u0002{which}\u0002 bulletin \u0002{n}\u0002.": "\u0002{which}\u0002-Bulletin \u0002{n}\u0002 wurde entfernt.", - "Removed all \u0002{n}\u0002 bot(s).": "Alle \u0002{n}\u0002 Bot(s) wurden entfernt.", "Removed badword pattern from \u0002{chan}\u0002.": "Schimpfwort-Muster wurde aus \u0002{chan}\u0002 entfernt.", "Removed fingerprint \u0002{fp}\u0002.": "Fingerprint \u0002{fp}\u0002 wurde entfernt.", "Removed forbidden pattern: {pattern}": "Verbotenes Muster entfernt: {pattern}", "Removed the forbid on {label} \u0002{mask}\u0002.": "Das Verbot für {label} \u0002{mask}\u0002 wurde entfernt.", - "Reopped \u0002{opped}\u0002 trusted regular(s) in \u0002{chan}\u0002.": "\u0002{opped}\u0002 vertraute(r) Stammgast/Stammgäste in \u0002{chan}\u0002 wurde(n) wieder geoppt.", "Report \u0002#{id}\u0002 ({state}):": "Meldung \u0002#{id}\u0002 ({state}):", "Report \u0002#{n}\u0002 closed.": "Meldung \u0002#{n}\u0002 geschlossen.", "Report \u0002#{n}\u0002 deleted.": "Meldung \u0002#{n}\u0002 gelöscht.", @@ -881,7 +862,6 @@ "The bot has left \u0002{chan}\u0002.": "Der Bot hat \u0002{chan}\u0002 verlassen.", "The bot may again kick channel operators in \u0002{chan}\u0002.": "Der Bot darf in \u0002{chan}\u0002 wieder Channel-Operatoren kicken.", "The bot may again kick voiced users in \u0002{chan}\u0002.": "Der Bot darf in \u0002{chan}\u0002 wieder Nutzer mit Voice kicken.", - "The bot will ban a user after \u0002{n}\u0002 kick(s) in \u0002{chan}\u0002.": "Der Bot bannt einen Nutzer nach \u0002{n}\u0002 Kick(s) in \u0002{chan}\u0002.", "The bot will kick without warning in \u0002{chan}\u0002.": "Der Bot kickt in \u0002{chan}\u0002 ohne Warnung.", "The bot will no longer kick channel operators in \u0002{chan}\u0002.": "Der Bot kickt in \u0002{chan}\u0002 keine Channel-Operatoren mehr.", "The bot will no longer kick voiced users in \u0002{chan}\u0002.": "Der Bot kickt in \u0002{chan}\u0002 keine Nutzer mit Voice mehr.", @@ -927,7 +907,6 @@ "Ticket \u0002#{id}\u0002 isn't open (or doesn't exist).": "Ticket \u0002#{id}\u0002 ist nicht offen (oder existiert nicht).", "Too many failed attempts. Please wait {secs}s and try again.": "Zu viele fehlgeschlagene Versuche. Bitte warte {secs}s und versuche es erneut.", "Top talkers:": "Aktivste Sprecher:", - "Top {shown} of {total} scored identit{suffix} for \u0002{chan}\u0002.": "Top {shown} von {total} bewerteten Identit{suffix} für \u0002{chan}\u0002.", "Topic for \u0002{chan}\u0002 updated.": "Thema für \u0002{chan}\u0002 aktualisiert.", "Trigger #\u0002{n}\u0002 removed from \u0002{chan}\u0002.": "Trigger #\u0002{n}\u0002 aus \u0002{chan}\u0002 entfernt.", "Triggers for \u0002{chan}\u0002 ({count}):": "Trigger für \u0002{chan}\u0002 ({count}):", @@ -958,8 +937,6 @@ "You can't ungroup your main account name.": "Du kannst deinen Hauptkontonamen nicht aus der Gruppe lösen.", "You cannot challenge yourself.": "Du kannst dich nicht selbst herausfordern.", "You don't have an account to confirm.": "Du hast kein Konto zum Bestätigen.", - "You have \u0002{total}\u0002 memo(s), \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Du hast \u0002{total}\u0002 Memo(s), \u0002{unread}\u0002 ungelesen, von maximal \u0002{max}\u0002.", - "You have \u0002{unread}\u0002 new memo(s). Read them with \u0002/msg MemoServ READ NEW\u0002.": "Du hast \u0002{unread}\u0002 neue Memo(s). Lies sie mit \u0002/msg MemoServ READ NEW\u0002.", "You have access on no channels.": "Du hast auf keinen Channels Zugriff.", "You have no games. Use \u0002CHALLENGE \u0002.": "Du hast keine Spiele. Verwende \u0002CHALLENGE \u0002.", "You have no memo #\u0002{n}\u0002.": "Du hast kein Memo #\u0002{n}\u0002.", @@ -1258,7 +1235,6 @@ "your mailbox summary": "deine Postfach-Übersicht", "your memo preferences": "deine Memo-Einstellungen", "{challenger} challenges you to {game} (game #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002": "{challenger} fordert dich zu {game} heraus (Spiel #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002", - "{count} ticket(s). \u0002VIEW\u0002 for detail.": "{count} Ticket(s). \u0002VIEW\u0002 für Details.", "{name} added for \u0002{mask}\u0002 (temporary).": "{name} für \u0002{mask}\u0002 hinzugefügt (temporär).", "{name} added for \u0002{mask}\u0002.": "{name} für \u0002{mask}\u0002 hinzugefügt.", "{name} for \u0002{mask}\u0002 removed.": "{name} für \u0002{mask}\u0002 entfernt.", @@ -1266,7 +1242,6 @@ "{name} updated for \u0002{mask}\u0002.": "{name} für \u0002{mask}\u0002 aktualisiert.", "{nick} declined your challenge #{id}.": "{nick} hat deine Herausforderung #{id} abgelehnt.", "{num}. {text} — by {by}": "{num}. {text} — von {by}", - "{n} report(s). \u0002VIEW\u0002 for detail.": "{n} Meldung(en). \u0002VIEW\u0002 für Details.", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}) — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] von {setter} ({when}) — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}), expires in {ttl} — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] von {setter} ({when}), läuft ab in {ttl} — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] — {reason}", @@ -1283,5 +1258,55 @@ "{who}: I have no record of \u0002{nick}\u0002 talking in \u0002{chan}\u0002.": "{who}: Ich habe keine Aufzeichnung, dass \u0002{nick}\u0002 in \u0002{chan}\u0002 gesprochen hat.", "{word} list for \u0002{name}\u0002:": "{word}-Liste für \u0002{name}\u0002:", "Type \u0002HELP \u0002 for detail on one command.": "Tippe \u0002HELP \u0002 für Details zu einem Befehl.", - "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "Keine Hilfe für \u0002{t}\u0002. Tippe \u0002HELP\u0002 für die Befehlsliste." + "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "Keine Hilfe für \u0002{t}\u0002. Tippe \u0002HELP\u0002 für die Befehlsliste.", + "Reopped \u0002{opped}\u0002 trusted regular in \u0002{chan}\u0002.": "\u0002{opped}\u0002 als vertrauten Stammgast in \u0002{chan}\u0002 wieder geoppt.", + "Reopped \u0002{opped}\u0002 trusted regulars in \u0002{chan}\u0002.": "\u0002{opped}\u0002 vertraute Stammgäste in \u0002{chan}\u0002 wieder geoppt.", + "Cleared \u0002{n}\u0002 trigger from \u0002{chan}\u0002.": "\u0002{n}\u0002 Trigger aus \u0002{chan}\u0002 entfernt.", + "Cleared \u0002{n}\u0002 triggers from \u0002{chan}\u0002.": "\u0002{n}\u0002 Trigger aus \u0002{chan}\u0002 entfernt.", + " Serving {count} channel: {list}": " Bediene {count} Channel: {list}", + " Serving {count} channels: {list}": " Bediene {count} Channels: {list}", + "Removed all \u0002{n}\u0002 bot.": "Alle \u0002{n}\u0002 Bot entfernt.", + "Removed all \u0002{n}\u0002 bots.": "Alle \u0002{n}\u0002 Bots entfernt.", + "Cleared \u0002{n}\u0002 badword pattern from \u0002{chan}\u0002.": "\u0002{n}\u0002 Badword-Muster aus \u0002{chan}\u0002 entfernt.", + "Cleared \u0002{n}\u0002 badword patterns from \u0002{chan}\u0002.": "\u0002{n}\u0002 Badword-Muster aus \u0002{chan}\u0002 entfernt.", + "The bot will ban a user after \u0002{n}\u0002 kick in \u0002{chan}\u0002.": "Der Bot bannt einen Benutzer nach \u0002{n}\u0002 Kick in \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kicks in \u0002{chan}\u0002.": "Der Bot bannt einen Benutzer nach \u0002{n}\u0002 Kicks in \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entry from \u0002{chan}\u0002's auto-kick list.": "\u0002{count}\u0002 Eintrag aus der Auto-Kick-Liste von \u0002{chan}\u0002 entfernt.", + "Cleared \u0002{count}\u0002 entries from \u0002{chan}\u0002's auto-kick list.": "\u0002{count}\u0002 Einträge aus der Auto-Kick-Liste von \u0002{chan}\u0002 entfernt.", + "Top {shown} of {total} scored identity for \u0002{chan}\u0002.": "Top {shown} von {total} bewerteter Identität für \u0002{chan}\u0002.", + "Top {shown} of {total} scored identities for \u0002{chan}\u0002.": "Top {shown} von {total} bewerteten Identitäten für \u0002{chan}\u0002.", + "\u0002{n}\u0002 vote will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 Stimme trägt in \u0002{chan}\u0002 künftig einen \u0002!votekick\u0002/\u0002!voteban\u0002.", + "\u0002{n}\u0002 votes will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 Stimmen tragen in \u0002{chan}\u0002 künftig einen \u0002!votekick\u0002/\u0002!voteban\u0002.", + "End of flags ({count} entry).": "Ende der Flags ({count} Eintrag).", + "End of flags ({count} entries).": "Ende der Flags ({count} Einträge).", + "Deleted all \u0002{count}\u0002 memo.": "Alle \u0002{count}\u0002 Memo gelöscht.", + "Deleted all \u0002{count}\u0002 memos.": "Alle \u0002{count}\u0002 Memos gelöscht.", + "Memo sent to \u0002{sent}\u0002 account.": "Memo an \u0002{sent}\u0002 Account gesendet.", + "Memo sent to \u0002{sent}\u0002 accounts.": "Memo an \u0002{sent}\u0002 Accounts gesendet.", + "End of session list ({count} IP).": "Ende der Sitzungsliste ({count} IP).", + "End of session list ({count} IPs).": "Ende der Sitzungsliste ({count} IPs).", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live session.": "\u0002{ip}\u0002 hat \u0002{count}\u0002 aktive Sitzung.", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live sessions.": "\u0002{ip}\u0002 hat \u0002{count}\u0002 aktive Sitzungen.", + "You have \u0002{total}\u0002 memo, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Du hast \u0002{total}\u0002 Memo, \u0002{unread}\u0002 ungelesen, von maximal \u0002{max}\u0002.", + "You have \u0002{total}\u0002 memos, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Du hast \u0002{total}\u0002 Memos, \u0002{unread}\u0002 ungelesen, von maximal \u0002{max}\u0002.", + "End of list — {count} match{more}.": "Ende der Liste — {count} Treffer{more}.", + "End of list — {count} matches{more}.": "Ende der Liste — {count} Treffer{more}.", + "Memo sent to \u0002{sent}\u0002 operator.": "Memo an \u0002{sent}\u0002 Operator gesendet.", + "Memo sent to \u0002{sent}\u0002 operators.": "Memo an \u0002{sent}\u0002 Operatoren gesendet.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host AKILL'd.": "CHANKILL auf \u0002{chan}\u0002: \u0002{banned}\u0002 Host per AKILL gesperrt.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 hosts AKILL'd.": "CHANKILL auf \u0002{chan}\u0002: \u0002{banned}\u0002 Hosts per AKILL gesperrt.", + " Auto-join : {count} channel — see \u0002AJOIN LIST\u0002": " Auto-Join : {count} Channel — siehe \u0002AJOIN LIST\u0002", + " Auto-join : {count} channels — see \u0002AJOIN LIST\u0002": " Auto-Join : {count} Channels — siehe \u0002AJOIN LIST\u0002", + "{count} ticket. \u0002VIEW\u0002 for detail.": "{count} Ticket. \u0002VIEW\u0002 für Details.", + "{count} tickets. \u0002VIEW\u0002 for detail.": "{count} Tickets. \u0002VIEW\u0002 für Details.", + "You have \u0002{unread}\u0002 new memo. Read it with \u0002/msg MemoServ READ NEW\u0002.": "Du hast \u0002{unread}\u0002 neues Memo. Lies es mit \u0002/msg MemoServ READ NEW\u0002.", + "You have \u0002{unread}\u0002 new memos. Read them with \u0002/msg MemoServ READ NEW\u0002.": "Du hast \u0002{unread}\u0002 neue Memos. Lies sie mit \u0002/msg MemoServ READ NEW\u0002.", + "Cleared \u0002{n}\u0002 notify watch.": "\u0002{n}\u0002 Notify-Beobachtung entfernt.", + "Cleared \u0002{n}\u0002 notify watches.": "\u0002{n}\u0002 Notify-Beobachtungen entfernt.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 line seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 Zeile gesehen.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 lines seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 Zeilen gesehen.", + "{n} report. \u0002VIEW\u0002 for detail.": "{n} Meldung. \u0002VIEW\u0002 für Details.", + "{n} reports. \u0002VIEW\u0002 for detail.": "{n} Meldungen. \u0002VIEW\u0002 für Details.", + "End of list ({count} group).": "Ende der Liste ({count} Gruppe).", + "End of list ({count} groups).": "Ende der Liste ({count} Gruppen)." } \ No newline at end of file diff --git a/lang/es-ar.json b/lang/es-ar.json index 1c2e228..20054c7 100644 --- a/lang/es-ar.json +++ b/lang/es-ar.json @@ -61,7 +61,6 @@ "\u0002{chan}\u0002 isn't suspended.": "\u0002{chan}\u0002 no está suspendido.", "\u0002{chan}\u0002 was already set that way.": "\u0002{chan}\u0002 ya estaba configurado así.", "\u0002{chan}\u0002 will no longer expire.": "\u0002{chan}\u0002 ya no va a expirar.", - "\u0002{chan}\u0002 — \u0002{lines}\u0002 line(s) seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 línea(s) vistas.", "\u0002{code}\u0002 isn't an available language. Available: {list}.": "\u0002{code}\u0002 no es un idioma disponible. Disponibles: {list}.", "\u0002{d}\u0002 isn't a valid expiry — try 30d, 12h, 45m, or 0.": "\u0002{d}\u0002 no es un vencimiento válido — probá 30d, 12h, 45m, o 0.", "\u0002{host}\u0002 is already in use. Please choose another.": "\u0002{host}\u0002 ya está en uso. Elegí otro.", @@ -69,7 +68,6 @@ "\u0002{host}\u0002 isn't a valid host (letters, digits, hyphens and dots).": "\u0002{host}\u0002 no es un host válido (letras, dígitos, guiones y puntos).", "\u0002{host}\u0002 isn't a valid host.": "\u0002{host}\u0002 no es un host válido.", "\u0002{host}\u0002 isn't allowed here. Please choose another.": "\u0002{host}\u0002 no está permitido acá. Elegí otro.", - "\u0002{ip}\u0002 has \u0002{count}\u0002 live session(s).": "\u0002{ip}\u0002 tiene \u0002{count}\u0002 sesión(es) activas.", "\u0002{label}\u0002 access on \u0002{chan}\u0002: \u0002{level}\u0002": "Acceso de \u0002{label}\u0002 en \u0002{chan}\u0002: \u0002{level}\u0002", "\u0002{mask}\u0002 isn't on \u0002{chan}\u0002's auto-kick list.": "\u0002{mask}\u0002 no está en la lista de auto-kick de \u0002{chan}\u0002.", "\u0002{mask}\u0002 isn't on the ignore list.": "\u0002{mask}\u0002 no está en la lista de ignorados.", @@ -88,7 +86,6 @@ "\u0002{nick}\u0002 isn't grouped to your account.": "\u0002{nick}\u0002 no está agrupado a tu cuenta.", "\u0002{nick}\u0002 isn't here.": "\u0002{nick}\u0002 no está acá.", "\u0002{nick}\u0002 was last seen {when} ({what}).": "\u0002{nick}\u0002 se vio por última vez {when} ({what}).", - "\u0002{n}\u0002 vote(s) will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 voto(s) ahora van a concretar un \u0002!votekick\u0002/\u0002!voteban\u0002 en \u0002{chan}\u0002.", "\u0002{pattern}\u0002 isn't a valid regular expression.": "\u0002{pattern}\u0002 no es una expresión regular válida.", "\u0002{raw}\u0002 isn't a valid \u0002{target}\u0002 mask.": "\u0002{raw}\u0002 no es una máscara \u0002{target}\u0002 válida.", "\u0002{target}\u0002 had no access to \u0002{chan}\u0002.": "\u0002{target}\u0002 no tenía acceso a \u0002{chan}\u0002.", @@ -143,7 +140,6 @@ " (custom)": " (personalizado)", " (no activity counters yet)": " (todavía no hay contadores de actividad)", " About : \u0002{target}\u0002": " Sobre : \u0002{target}\u0002", - " Auto-join : {count} channel(s) — see \u0002AJOIN LIST\u0002": " Auto-join : {count} canal(es) — mirá \u0002AJOIN LIST\u0002", " By : \u0002{by}\u0002": " Por : \u0002{by}\u0002", " Description: {desc}": " Descripción: {desc}", " Email : (none set)": " Email : (ninguno)", @@ -165,7 +161,6 @@ " Real name: {gecos}": " Nombre real: {gecos}", " Reason : {reason}": " Motivo : {reason}", " Registered : {when}": " Registrado : {when}", - " Serving {count} channel(s): {list}": " Atendiendo {count} canal(es): {list}", " Staff note : {note}": " Nota del staff: {note}", " Suspended : by \u0002{by}\u0002 — {reason}": " Suspendido : por \u0002{by}\u0002 — {reason}", " URL : {url}": " URL : {url}", @@ -278,7 +273,6 @@ "Both channels must be registered.": "Los dos canales tienen que estar registrados.", "Bots ({count}):": "Bots ({count}):", "CALC and show each die": "CALC y mostrar cada dado", - "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) AKILL'd.": "CHANKILL en \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) AKILLeados.", "Can't activate: \u0002{host}\u0002 is on the forbidden list.": "No se puede activar: \u0002{host}\u0002 está en la lista de prohibidos.", "Can't activate: {msg}": "No se puede activar: {msg}", "Challenge \u0002#{id}\u0002 ({game}) sent to \u0002{target}\u0002.": "Desafío \u0002#{id}\u0002 ({game}) enviado a \u0002{target}\u0002.", @@ -291,10 +285,6 @@ "Channels released: {list}.": "Canales liberados: {list}.", "Channels you have access on ({count}):": "Canales en los que tenés acceso ({count}):", "Classement \u0002{game}\u0002 vide. Sois le premier à gagner !": "Ranking de \u0002{game}\u0002 vacío. ¡Sé el primero en ganar!", - "Cleared \u0002{count}\u0002 entr{suffix} from \u0002{chan}\u0002's auto-kick list.": "Se borraron \u0002{count}\u0002 entrada{suffix} de la lista de auto-kick de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 badword pattern(s) from \u0002{chan}\u0002.": "Se borraron \u0002{n}\u0002 patrón(es) de badword de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 notify watch(es).": "Se borraron \u0002{n}\u0002 vigilancia(s) de notify.", - "Cleared \u0002{n}\u0002 trigger(s) from \u0002{chan}\u0002.": "Se borraron \u0002{n}\u0002 trigger(s) de \u0002{chan}\u0002.", "Cleared \u0002{target}\u0002's access to \u0002{chan}\u0002.": "Se borró el acceso de \u0002{target}\u0002 a \u0002{chan}\u0002.", "Cleared the *!*@{host} ban on \u0002{chan}\u0002.": "Se sacó el ban *!*@{host} en \u0002{chan}\u0002.", "Contact email for \u0002{chan}\u0002 cleared.": "Se borró el email de contacto de \u0002{chan}\u0002.", @@ -306,22 +296,17 @@ "Couldn't update \u0002{chan}\u0002 — please try again in a moment.": "No se pudo actualizar \u0002{chan}\u0002 — probá de nuevo en un momento.", "DEFCON set to \u0002{level}\u0002 — {desc}.": "DEFCON configurado en \u0002{level}\u0002 — {desc}.", "Declined.": "Rechazado.", - "Deleted all \u0002{count}\u0002 memo(s).": "Se borraron todos los \u0002{count}\u0002 memo(s).", "Description for \u0002{chan}\u0002 updated.": "Se actualizó la descripción de \u0002{chan}\u0002.", "DiceServ rolls dice and evaluates maths for tabletop games. Dice: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Also + - * / ^ %, parentheses, functions (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, and \u00023~2d6\u0002 to repeat.": "DiceServ tira dados y evalúa cuentas matemáticas para juegos de mesa. Dados: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. También + - * / ^ %, paréntesis, funciones (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, y \u00023~2d6\u0002 para repetir.", "DictServ looks words up in the dict.org dictionaries. Try \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (thesaurus), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, and more — \u0002LOOKUP\u0002 lists them all.": "DictServ busca palabras en los diccionarios de dict.org. Probá \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (tesauro), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, y más — \u0002LOOKUP\u0002 los lista todos.", "Email for \u0002{account}\u0002 cleared.": "Se borró el email de \u0002{account}\u0002.", "Email for \u0002{account}\u0002 updated.": "Se actualizó el email de \u0002{account}\u0002.", "End of exception list ({count} shown).": "Fin de la lista de excepciones ({count} mostradas).", - "End of flags ({count} entr{suffix}).": "Fin de los flags ({count} entrada{suffix}).", "End of ignore list ({count} shown).": "Fin de la lista de ignorados ({count} mostrados).", "End of jupe list ({count} shown).": "Fin de la lista de jupes ({count} mostrados).", - "End of list ({count} group(s)).": "Fin de la lista ({count} grupo(s)).", - "End of list — {count} match(es){more}.": "Fin de la lista — {count} coincidencia(s){more}.", "End of notify list ({shown} shown).": "Fin de la lista de notify ({shown} mostrados).", "End of results ({count} shown, newest first — refine the search to narrow).": "Fin de los resultados ({count} mostrados, más nuevos primero — refiná la búsqueda para acotar).", "End of runtime operator list ({count} shown).": "Fin de la lista de operadores de runtime ({count} mostrados).", - "End of session list ({count} IP(s)).": "Fin de la lista de sesiones ({count} IP(s)).", "End of spam-filter list ({shown} shown).": "Fin de la lista de spam-filter ({shown} mostrados).", "End of {name} list ({shown} shown).": "Fin de la lista de {name} ({shown} mostrados).", "End of {which} bulletins ({count} shown).": "Fin de los boletines {which} ({count} mostrados).", @@ -387,8 +372,6 @@ "Mask must be a \u0002#channel\u0002, a \u0002nick!user@host\u0002 pattern, or a bare nick glob — and no spaces.": "La máscara tiene que ser un \u0002#channel\u0002, un patrón \u0002nick!user@host\u0002, o un glob de nick pelado — y sin espacios.", "Memo #\u0002{num}\u0002 from \u0002{from}\u0002 ({when}):": "Memo #\u0002{num}\u0002 de \u0002{from}\u0002 ({when}):", "Memo #\u0002{n}\u0002 deleted.": "Memo #\u0002{n}\u0002 eliminado.", - "Memo sent to \u0002{sent}\u0002 account(s).": "Memo enviado a \u0002{sent}\u0002 cuenta(s).", - "Memo sent to \u0002{sent}\u0002 operator(s).": "Memo enviado a \u0002{sent}\u0002 operador(es).", "Memo sent to \u0002{target}\u0002.": "Memo enviado a \u0002{target}\u0002.", "MemoServ delivers messages to registered users, online or not. You must be identified to use it.": "MemoServ entrega mensajes a usuarios registrados, estén conectados o no. Tenés que estar identificado para usarlo.", "Mode lock for \u0002{chan}\u0002 updated.": "Se actualizó el mode lock de \u0002{chan}\u0002.", @@ -485,12 +468,10 @@ "Removed \u0002{mask}\u0002 from \u0002{chan}\u0002's auto-kick list.": "Se quitó \u0002{mask}\u0002 de la lista de auto-kick de \u0002{chan}\u0002.", "Removed \u0002{target}\u0002 from \u0002{name}\u0002.": "Se quitó \u0002{target}\u0002 de \u0002{name}\u0002.", "Removed \u0002{which}\u0002 bulletin \u0002{n}\u0002.": "Se eliminó el boletín \u0002{which}\u0002 \u0002{n}\u0002.", - "Removed all \u0002{n}\u0002 bot(s).": "Se quitaron los \u0002{n}\u0002 bot(s).", "Removed badword pattern from \u0002{chan}\u0002.": "Se quitó el patrón de palabra prohibida de \u0002{chan}\u0002.", "Removed fingerprint \u0002{fp}\u0002.": "Se quitó la huella \u0002{fp}\u0002.", "Removed forbidden pattern: {pattern}": "Se quitó el patrón prohibido: {pattern}", "Removed the forbid on {label} \u0002{mask}\u0002.": "Se quitó la prohibición sobre {label} \u0002{mask}\u0002.", - "Reopped \u0002{opped}\u0002 trusted regular(s) in \u0002{chan}\u0002.": "Se volvió a dar op a \u0002{opped}\u0002 habitué(s) de confianza en \u0002{chan}\u0002.", "Report \u0002#{id}\u0002 ({state}):": "Reporte \u0002#{id}\u0002 ({state}):", "Report \u0002#{n}\u0002 closed.": "Reporte \u0002#{n}\u0002 cerrado.", "Report \u0002#{n}\u0002 deleted.": "Reporte \u0002#{n}\u0002 eliminado.", @@ -881,7 +862,6 @@ "The bot has left \u0002{chan}\u0002.": "El bot salió de \u0002{chan}\u0002.", "The bot may again kick channel operators in \u0002{chan}\u0002.": "El bot puede volver a expulsar a los operadores del canal en \u0002{chan}\u0002.", "The bot may again kick voiced users in \u0002{chan}\u0002.": "El bot puede volver a expulsar a los usuarios con voz en \u0002{chan}\u0002.", - "The bot will ban a user after \u0002{n}\u0002 kick(s) in \u0002{chan}\u0002.": "El bot va a banear a un usuario después de \u0002{n}\u0002 expulsión(es) en \u0002{chan}\u0002.", "The bot will kick without warning in \u0002{chan}\u0002.": "El bot va a expulsar sin aviso en \u0002{chan}\u0002.", "The bot will no longer kick channel operators in \u0002{chan}\u0002.": "El bot ya no va a expulsar a los operadores del canal en \u0002{chan}\u0002.", "The bot will no longer kick voiced users in \u0002{chan}\u0002.": "El bot ya no va a expulsar a los usuarios con voz en \u0002{chan}\u0002.", @@ -927,7 +907,6 @@ "Ticket \u0002#{id}\u0002 isn't open (or doesn't exist).": "El ticket \u0002#{id}\u0002 no está abierto (o no existe).", "Too many failed attempts. Please wait {secs}s and try again.": "Demasiados intentos fallidos. Esperá {secs}s y probá de nuevo.", "Top talkers:": "Los que más hablan:", - "Top {shown} of {total} scored identit{suffix} for \u0002{chan}\u0002.": "Top {shown} de {total} identidad{suffix} con puntaje para \u0002{chan}\u0002.", "Topic for \u0002{chan}\u0002 updated.": "Tema de \u0002{chan}\u0002 actualizado.", "Trigger #\u0002{n}\u0002 removed from \u0002{chan}\u0002.": "Trigger #\u0002{n}\u0002 eliminado de \u0002{chan}\u0002.", "Triggers for \u0002{chan}\u0002 ({count}):": "Triggers de \u0002{chan}\u0002 ({count}):", @@ -958,8 +937,6 @@ "You can't ungroup your main account name.": "No podés desagrupar el nombre principal de tu cuenta.", "You cannot challenge yourself.": "No podés desafiarte a vos mismo.", "You don't have an account to confirm.": "No tenés una cuenta para confirmar.", - "You have \u0002{total}\u0002 memo(s), \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Tenés \u0002{total}\u0002 memo(s), \u0002{unread}\u0002 sin leer, de un máximo de \u0002{max}\u0002.", - "You have \u0002{unread}\u0002 new memo(s). Read them with \u0002/msg MemoServ READ NEW\u0002.": "Tenés \u0002{unread}\u0002 memo(s) nuevo(s). Leelos con \u0002/msg MemoServ READ NEW\u0002.", "You have access on no channels.": "No tenés acceso en ningún canal.", "You have no games. Use \u0002CHALLENGE \u0002.": "No tenés juegos. Usá \u0002CHALLENGE \u0002.", "You have no memo #\u0002{n}\u0002.": "No tenés el memo #\u0002{n}\u0002.", @@ -1258,7 +1235,6 @@ "your mailbox summary": "el resumen de tu buzón", "your memo preferences": "tus preferencias de memos", "{challenger} challenges you to {game} (game #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002": "{challenger} te desafía a {game} (juego #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002", - "{count} ticket(s). \u0002VIEW\u0002 for detail.": "{count} ticket(s). \u0002VIEW\u0002 para el detalle.", "{name} added for \u0002{mask}\u0002 (temporary).": "{name} agregado para \u0002{mask}\u0002 (temporal).", "{name} added for \u0002{mask}\u0002.": "{name} agregado para \u0002{mask}\u0002.", "{name} for \u0002{mask}\u0002 removed.": "{name} para \u0002{mask}\u0002 eliminado.", @@ -1266,7 +1242,6 @@ "{name} updated for \u0002{mask}\u0002.": "{name} actualizado para \u0002{mask}\u0002.", "{nick} declined your challenge #{id}.": "{nick} rechazó tu desafío #{id}.", "{num}. {text} — by {by}": "{num}. {text} — por {by}", - "{n} report(s). \u0002VIEW\u0002 for detail.": "{n} reporte(s). \u0002VIEW\u0002 para el detalle.", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}) — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] por {setter} ({when}) — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}), expires in {ttl} — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] por {setter} ({when}), vence en {ttl} — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] — {reason}", @@ -1283,5 +1258,55 @@ "{who}: I have no record of \u0002{nick}\u0002 talking in \u0002{chan}\u0002.": "{who}: No tengo registro de que \u0002{nick}\u0002 haya hablado en \u0002{chan}\u0002.", "{word} list for \u0002{name}\u0002:": "Lista de {word} para \u0002{name}\u0002:", "Type \u0002HELP \u0002 for detail on one command.": "Escribí \u0002HELP \u0002 para ver el detalle de un comando.", - "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "No hay ayuda para \u0002{t}\u0002. Escribí \u0002HELP\u0002 para ver la lista de comandos." + "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "No hay ayuda para \u0002{t}\u0002. Escribí \u0002HELP\u0002 para ver la lista de comandos.", + "Reopped \u0002{opped}\u0002 trusted regular in \u0002{chan}\u0002.": "Se reopeó \u0002{opped}\u0002 habitual de confianza en \u0002{chan}\u0002.", + "Reopped \u0002{opped}\u0002 trusted regulars in \u0002{chan}\u0002.": "Se reopearon \u0002{opped}\u0002 habituales de confianza en \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 trigger from \u0002{chan}\u0002.": "Se borró \u0002{n}\u0002 disparador de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 triggers from \u0002{chan}\u0002.": "Se borraron \u0002{n}\u0002 disparadores de \u0002{chan}\u0002.", + " Serving {count} channel: {list}": " Atendiendo {count} canal: {list}", + " Serving {count} channels: {list}": " Atendiendo {count} canales: {list}", + "Removed all \u0002{n}\u0002 bot.": "Se eliminó \u0002{n}\u0002 bot en total.", + "Removed all \u0002{n}\u0002 bots.": "Se eliminaron los \u0002{n}\u0002 bots.", + "Cleared \u0002{n}\u0002 badword pattern from \u0002{chan}\u0002.": "Se borró \u0002{n}\u0002 patrón de palabra prohibida de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 badword patterns from \u0002{chan}\u0002.": "Se borraron \u0002{n}\u0002 patrones de palabras prohibidas de \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kick in \u0002{chan}\u0002.": "El bot va a banear a un usuario después de \u0002{n}\u0002 kick en \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kicks in \u0002{chan}\u0002.": "El bot va a banear a un usuario después de \u0002{n}\u0002 kicks en \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entry from \u0002{chan}\u0002's auto-kick list.": "Se borró \u0002{count}\u0002 entrada de la lista de auto-kick de \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entries from \u0002{chan}\u0002's auto-kick list.": "Se borraron \u0002{count}\u0002 entradas de la lista de auto-kick de \u0002{chan}\u0002.", + "Top {shown} of {total} scored identity for \u0002{chan}\u0002.": "Top {shown} de {total} identidad puntuada para \u0002{chan}\u0002.", + "Top {shown} of {total} scored identities for \u0002{chan}\u0002.": "Top {shown} de {total} identidades puntuadas para \u0002{chan}\u0002.", + "\u0002{n}\u0002 vote will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 voto va a alcanzar ahora para un \u0002!votekick\u0002/\u0002!voteban\u0002 en \u0002{chan}\u0002.", + "\u0002{n}\u0002 votes will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 votos van a alcanzar ahora para un \u0002!votekick\u0002/\u0002!voteban\u0002 en \u0002{chan}\u0002.", + "End of flags ({count} entry).": "Fin de los flags ({count} entrada).", + "End of flags ({count} entries).": "Fin de los flags ({count} entradas).", + "Deleted all \u0002{count}\u0002 memo.": "Se eliminaron \u0002{count}\u0002 memo en total.", + "Deleted all \u0002{count}\u0002 memos.": "Se eliminaron los \u0002{count}\u0002 memos.", + "Memo sent to \u0002{sent}\u0002 account.": "Memo enviado a \u0002{sent}\u0002 cuenta.", + "Memo sent to \u0002{sent}\u0002 accounts.": "Memo enviado a \u0002{sent}\u0002 cuentas.", + "End of session list ({count} IP).": "Fin de la lista de sesiones ({count} IP).", + "End of session list ({count} IPs).": "Fin de la lista de sesiones ({count} IPs).", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live session.": "\u0002{ip}\u0002 tiene \u0002{count}\u0002 sesión activa.", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live sessions.": "\u0002{ip}\u0002 tiene \u0002{count}\u0002 sesiones activas.", + "You have \u0002{total}\u0002 memo, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Tenés \u0002{total}\u0002 memo, \u0002{unread}\u0002 sin leer, de un máximo de \u0002{max}\u0002.", + "You have \u0002{total}\u0002 memos, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Tenés \u0002{total}\u0002 memos, \u0002{unread}\u0002 sin leer, de un máximo de \u0002{max}\u0002.", + "End of list — {count} match{more}.": "Fin de la lista — {count} coincidencia{more}.", + "End of list — {count} matches{more}.": "Fin de la lista — {count} coincidencias{more}.", + "Memo sent to \u0002{sent}\u0002 operator.": "Memo enviado a \u0002{sent}\u0002 operador.", + "Memo sent to \u0002{sent}\u0002 operators.": "Memo enviado a \u0002{sent}\u0002 operadores.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host AKILL'd.": "CHANKILL en \u0002{chan}\u0002: se aplicó AKILL a \u0002{banned}\u0002 host.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 hosts AKILL'd.": "CHANKILL en \u0002{chan}\u0002: se aplicó AKILL a \u0002{banned}\u0002 hosts.", + " Auto-join : {count} channel — see \u0002AJOIN LIST\u0002": " Auto-join : {count} canal — mirá \u0002AJOIN LIST\u0002", + " Auto-join : {count} channels — see \u0002AJOIN LIST\u0002": " Auto-join : {count} canales — mirá \u0002AJOIN LIST\u0002", + "{count} ticket. \u0002VIEW\u0002 for detail.": "{count} ticket. \u0002VIEW\u0002 para el detalle.", + "{count} tickets. \u0002VIEW\u0002 for detail.": "{count} tickets. \u0002VIEW\u0002 para el detalle.", + "You have \u0002{unread}\u0002 new memo. Read it with \u0002/msg MemoServ READ NEW\u0002.": "Tenés \u0002{unread}\u0002 memo nuevo. Leelo con \u0002/msg MemoServ READ NEW\u0002.", + "You have \u0002{unread}\u0002 new memos. Read them with \u0002/msg MemoServ READ NEW\u0002.": "Tenés \u0002{unread}\u0002 memos nuevos. Leelos con \u0002/msg MemoServ READ NEW\u0002.", + "Cleared \u0002{n}\u0002 notify watch.": "Se borró \u0002{n}\u0002 seguimiento de notificación.", + "Cleared \u0002{n}\u0002 notify watches.": "Se borraron \u0002{n}\u0002 seguimientos de notificación.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 line seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 línea vista.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 lines seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 líneas vistas.", + "{n} report. \u0002VIEW\u0002 for detail.": "{n} reporte. \u0002VIEW\u0002 para el detalle.", + "{n} reports. \u0002VIEW\u0002 for detail.": "{n} reportes. \u0002VIEW\u0002 para el detalle.", + "End of list ({count} group).": "Fin de la lista ({count} grupo).", + "End of list ({count} groups).": "Fin de la lista ({count} grupos)." } \ No newline at end of file diff --git a/lang/es.json b/lang/es.json index 2accadf..5194a24 100644 --- a/lang/es.json +++ b/lang/es.json @@ -61,7 +61,6 @@ "\u0002{chan}\u0002 isn't suspended.": "\u0002{chan}\u0002 no está suspendido.", "\u0002{chan}\u0002 was already set that way.": "\u0002{chan}\u0002 ya estaba configurado así.", "\u0002{chan}\u0002 will no longer expire.": "\u0002{chan}\u0002 ya no caducará.", - "\u0002{chan}\u0002 — \u0002{lines}\u0002 line(s) seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 línea(s) vistas.", "\u0002{code}\u0002 isn't an available language. Available: {list}.": "\u0002{code}\u0002 no es un idioma disponible. Disponibles: {list}.", "\u0002{d}\u0002 isn't a valid expiry — try 30d, 12h, 45m, or 0.": "\u0002{d}\u0002 no es una caducidad válida — prueba 30d, 12h, 45m o 0.", "\u0002{host}\u0002 is already in use. Please choose another.": "\u0002{host}\u0002 ya está en uso. Por favor, elige otro.", @@ -69,7 +68,6 @@ "\u0002{host}\u0002 isn't a valid host (letters, digits, hyphens and dots).": "\u0002{host}\u0002 no es un host válido (letras, dígitos, guiones y puntos).", "\u0002{host}\u0002 isn't a valid host.": "\u0002{host}\u0002 no es un host válido.", "\u0002{host}\u0002 isn't allowed here. Please choose another.": "\u0002{host}\u0002 no está permitido aquí. Por favor, elige otro.", - "\u0002{ip}\u0002 has \u0002{count}\u0002 live session(s).": "\u0002{ip}\u0002 tiene \u0002{count}\u0002 sesión(es) activa(s).", "\u0002{label}\u0002 access on \u0002{chan}\u0002: \u0002{level}\u0002": "\u0002{label}\u0002 acceso en \u0002{chan}\u0002: \u0002{level}\u0002", "\u0002{mask}\u0002 isn't on \u0002{chan}\u0002's auto-kick list.": "\u0002{mask}\u0002 no está en la lista de auto-kick de \u0002{chan}\u0002.", "\u0002{mask}\u0002 isn't on the ignore list.": "\u0002{mask}\u0002 no está en la lista de ignorados.", @@ -88,7 +86,6 @@ "\u0002{nick}\u0002 isn't grouped to your account.": "\u0002{nick}\u0002 no está agrupado a tu cuenta.", "\u0002{nick}\u0002 isn't here.": "\u0002{nick}\u0002 no está aquí.", "\u0002{nick}\u0002 was last seen {when} ({what}).": "\u0002{nick}\u0002 se vio por última vez {when} ({what}).", - "\u0002{n}\u0002 vote(s) will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 voto(s) ahora activarán un \u0002!votekick\u0002/\u0002!voteban\u0002 en \u0002{chan}\u0002.", "\u0002{pattern}\u0002 isn't a valid regular expression.": "\u0002{pattern}\u0002 no es una expresión regular válida.", "\u0002{raw}\u0002 isn't a valid \u0002{target}\u0002 mask.": "\u0002{raw}\u0002 no es una máscara \u0002{target}\u0002 válida.", "\u0002{target}\u0002 had no access to \u0002{chan}\u0002.": "\u0002{target}\u0002 no tenía acceso a \u0002{chan}\u0002.", @@ -143,7 +140,6 @@ " (custom)": " (personalizado)", " (no activity counters yet)": " (aún no hay contadores de actividad)", " About : \u0002{target}\u0002": " Acerca de : \u0002{target}\u0002", - " Auto-join : {count} channel(s) — see \u0002AJOIN LIST\u0002": " Auto-join : {count} canal(es) — ver \u0002AJOIN LIST\u0002", " By : \u0002{by}\u0002": " Por : \u0002{by}\u0002", " Description: {desc}": " Descripción: {desc}", " Email : (none set)": " Email : (no establecido)", @@ -165,7 +161,6 @@ " Real name: {gecos}": " Nombre real: {gecos}", " Reason : {reason}": " Motivo : {reason}", " Registered : {when}": " Registrado : {when}", - " Serving {count} channel(s): {list}": " Atendiendo {count} canal(es): {list}", " Staff note : {note}": " Nota del staff: {note}", " Suspended : by \u0002{by}\u0002 — {reason}": " Suspendido : por \u0002{by}\u0002 — {reason}", " URL : {url}": " URL : {url}", @@ -278,7 +273,6 @@ "Both channels must be registered.": "Ambos canales deben estar registrados.", "Bots ({count}):": "Bots ({count}):", "CALC and show each die": "CALC y mostrar cada dado", - "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) AKILL'd.": "CHANKILL en \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) con AKILL aplicado.", "Can't activate: \u0002{host}\u0002 is on the forbidden list.": "No se puede activar: \u0002{host}\u0002 está en la lista de prohibidos.", "Can't activate: {msg}": "No se puede activar: {msg}", "Challenge \u0002#{id}\u0002 ({game}) sent to \u0002{target}\u0002.": "Desafío \u0002#{id}\u0002 ({game}) enviado a \u0002{target}\u0002.", @@ -291,10 +285,6 @@ "Channels released: {list}.": "Canales liberados: {list}.", "Channels you have access on ({count}):": "Canales a los que tienes acceso ({count}):", "Classement \u0002{game}\u0002 vide. Sois le premier à gagner !": "Clasificación \u0002{game}\u0002 vacía. ¡Sé el primero en ganar!", - "Cleared \u0002{count}\u0002 entr{suffix} from \u0002{chan}\u0002's auto-kick list.": "Se borraron \u0002{count}\u0002 entr{suffix} de la lista de auto-kick de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 badword pattern(s) from \u0002{chan}\u0002.": "Se borraron \u0002{n}\u0002 patrón(es) de palabras prohibidas de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 notify watch(es).": "Se borraron \u0002{n}\u0002 vigilancia(s) de notificación.", - "Cleared \u0002{n}\u0002 trigger(s) from \u0002{chan}\u0002.": "Se borraron \u0002{n}\u0002 disparador(es) de \u0002{chan}\u0002.", "Cleared \u0002{target}\u0002's access to \u0002{chan}\u0002.": "Se borró el acceso de \u0002{target}\u0002 a \u0002{chan}\u0002.", "Cleared the *!*@{host} ban on \u0002{chan}\u0002.": "Se borró el ban *!*@{host} en \u0002{chan}\u0002.", "Contact email for \u0002{chan}\u0002 cleared.": "Email de contacto de \u0002{chan}\u0002 borrado.", @@ -306,22 +296,17 @@ "Couldn't update \u0002{chan}\u0002 — please try again in a moment.": "No se pudo actualizar \u0002{chan}\u0002 — por favor inténtalo de nuevo en un momento.", "DEFCON set to \u0002{level}\u0002 — {desc}.": "DEFCON establecido en \u0002{level}\u0002 — {desc}.", "Declined.": "Rechazado.", - "Deleted all \u0002{count}\u0002 memo(s).": "Se eliminaron todos los \u0002{count}\u0002 memo(s).", "Description for \u0002{chan}\u0002 updated.": "Descripción de \u0002{chan}\u0002 actualizada.", "DiceServ rolls dice and evaluates maths for tabletop games. Dice: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Also + - * / ^ %, parentheses, functions (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, and \u00023~2d6\u0002 to repeat.": "DiceServ tira dados y evalúa operaciones matemáticas para juegos de mesa. Dados: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. También + - * / ^ %, paréntesis, funciones (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, y \u00023~2d6\u0002 para repetir.", "DictServ looks words up in the dict.org dictionaries. Try \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (thesaurus), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, and more — \u0002LOOKUP\u0002 lists them all.": "DictServ busca palabras en los diccionarios de dict.org. Prueba \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (tesauro), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, y más — \u0002LOOKUP\u0002 los lista todos.", "Email for \u0002{account}\u0002 cleared.": "Email de \u0002{account}\u0002 borrado.", "Email for \u0002{account}\u0002 updated.": "Email de \u0002{account}\u0002 actualizado.", "End of exception list ({count} shown).": "Fin de la lista de excepciones ({count} mostradas).", - "End of flags ({count} entr{suffix}).": "Fin de los flags ({count} entr{suffix}).", "End of ignore list ({count} shown).": "Fin de la lista de ignorados ({count} mostrados).", "End of jupe list ({count} shown).": "Fin de la lista de jupes ({count} mostrados).", - "End of list ({count} group(s)).": "Fin de la lista ({count} grupo(s)).", - "End of list — {count} match(es){more}.": "Fin de la lista — {count} coincidencia(s){more}.", "End of notify list ({shown} shown).": "Fin de la lista de notificaciones ({shown} mostradas).", "End of results ({count} shown, newest first — refine the search to narrow).": "Fin de los resultados ({count} mostrados, más recientes primero — afina la búsqueda para acotar).", "End of runtime operator list ({count} shown).": "Fin de la lista de operadores en tiempo de ejecución ({count} mostrados).", - "End of session list ({count} IP(s)).": "Fin de la lista de sesiones ({count} IP(s)).", "End of spam-filter list ({shown} shown).": "Fin de la lista de filtros de spam ({shown} mostrados).", "End of {name} list ({shown} shown).": "Fin de la lista {name} ({shown} mostrados).", "End of {which} bulletins ({count} shown).": "Fin de los boletines {which} ({count} mostrados).", @@ -387,8 +372,6 @@ "Mask must be a \u0002#channel\u0002, a \u0002nick!user@host\u0002 pattern, or a bare nick glob — and no spaces.": "La máscara debe ser un \u0002#channel\u0002, un patrón \u0002nick!user@host\u0002, o un glob de nick simple — y sin espacios.", "Memo #\u0002{num}\u0002 from \u0002{from}\u0002 ({when}):": "Memo #\u0002{num}\u0002 de \u0002{from}\u0002 ({when}):", "Memo #\u0002{n}\u0002 deleted.": "Memo #\u0002{n}\u0002 eliminado.", - "Memo sent to \u0002{sent}\u0002 account(s).": "Memo enviado a \u0002{sent}\u0002 cuenta(s).", - "Memo sent to \u0002{sent}\u0002 operator(s).": "Memo enviado a \u0002{sent}\u0002 operador(es).", "Memo sent to \u0002{target}\u0002.": "Memo enviado a \u0002{target}\u0002.", "MemoServ delivers messages to registered users, online or not. You must be identified to use it.": "MemoServ entrega mensajes a usuarios registrados, estén conectados o no. Debes estar identificado para usarlo.", "Mode lock for \u0002{chan}\u0002 updated.": "Bloqueo de modos de \u0002{chan}\u0002 actualizado.", @@ -485,12 +468,10 @@ "Removed \u0002{mask}\u0002 from \u0002{chan}\u0002's auto-kick list.": "Se quitó \u0002{mask}\u0002 de la lista de auto-kick de \u0002{chan}\u0002.", "Removed \u0002{target}\u0002 from \u0002{name}\u0002.": "Se quitó \u0002{target}\u0002 de \u0002{name}\u0002.", "Removed \u0002{which}\u0002 bulletin \u0002{n}\u0002.": "Se eliminó el boletín \u0002{which}\u0002 \u0002{n}\u0002.", - "Removed all \u0002{n}\u0002 bot(s).": "Se quitaron los \u0002{n}\u0002 bot(s).", "Removed badword pattern from \u0002{chan}\u0002.": "Se quitó el patrón de palabra prohibida de \u0002{chan}\u0002.", "Removed fingerprint \u0002{fp}\u0002.": "Se quitó la huella \u0002{fp}\u0002.", "Removed forbidden pattern: {pattern}": "Se quitó el patrón prohibido: {pattern}", "Removed the forbid on {label} \u0002{mask}\u0002.": "Se quitó la prohibición sobre {label} \u0002{mask}\u0002.", - "Reopped \u0002{opped}\u0002 trusted regular(s) in \u0002{chan}\u0002.": "Se volvió a dar op a \u0002{opped}\u0002 regular(es) de confianza en \u0002{chan}\u0002.", "Report \u0002#{id}\u0002 ({state}):": "Reporte \u0002#{id}\u0002 ({state}):", "Report \u0002#{n}\u0002 closed.": "Reporte \u0002#{n}\u0002 cerrado.", "Report \u0002#{n}\u0002 deleted.": "Reporte \u0002#{n}\u0002 eliminado.", @@ -881,7 +862,6 @@ "The bot has left \u0002{chan}\u0002.": "El bot ha salido de \u0002{chan}\u0002.", "The bot may again kick channel operators in \u0002{chan}\u0002.": "El bot puede volver a expulsar a los operadores del canal en \u0002{chan}\u0002.", "The bot may again kick voiced users in \u0002{chan}\u0002.": "El bot puede volver a expulsar a los usuarios con voz en \u0002{chan}\u0002.", - "The bot will ban a user after \u0002{n}\u0002 kick(s) in \u0002{chan}\u0002.": "El bot vetará a un usuario tras \u0002{n}\u0002 expulsión(es) en \u0002{chan}\u0002.", "The bot will kick without warning in \u0002{chan}\u0002.": "El bot expulsará sin avisar en \u0002{chan}\u0002.", "The bot will no longer kick channel operators in \u0002{chan}\u0002.": "El bot ya no expulsará a los operadores del canal en \u0002{chan}\u0002.", "The bot will no longer kick voiced users in \u0002{chan}\u0002.": "El bot ya no expulsará a los usuarios con voz en \u0002{chan}\u0002.", @@ -927,7 +907,6 @@ "Ticket \u0002#{id}\u0002 isn't open (or doesn't exist).": "El ticket \u0002#{id}\u0002 no está abierto (o no existe).", "Too many failed attempts. Please wait {secs}s and try again.": "Demasiados intentos fallidos. Espera {secs}s e inténtalo de nuevo.", "Top talkers:": "Los que más hablan:", - "Top {shown} of {total} scored identit{suffix} for \u0002{chan}\u0002.": "Top {shown} de {total} identidad{suffix} puntuadas para \u0002{chan}\u0002.", "Topic for \u0002{chan}\u0002 updated.": "Tema de \u0002{chan}\u0002 actualizado.", "Trigger #\u0002{n}\u0002 removed from \u0002{chan}\u0002.": "Disparador #\u0002{n}\u0002 eliminado de \u0002{chan}\u0002.", "Triggers for \u0002{chan}\u0002 ({count}):": "Disparadores de \u0002{chan}\u0002 ({count}):", @@ -958,8 +937,6 @@ "You can't ungroup your main account name.": "No puedes desagrupar el nombre de tu cuenta principal.", "You cannot challenge yourself.": "No puedes desafiarte a ti mismo.", "You don't have an account to confirm.": "No tienes ninguna cuenta que confirmar.", - "You have \u0002{total}\u0002 memo(s), \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Tienes \u0002{total}\u0002 memo(s), \u0002{unread}\u0002 sin leer, de un máximo de \u0002{max}\u0002.", - "You have \u0002{unread}\u0002 new memo(s). Read them with \u0002/msg MemoServ READ NEW\u0002.": "Tienes \u0002{unread}\u0002 memo(s) nuevo(s). Léelos con \u0002/msg MemoServ READ NEW\u0002.", "You have access on no channels.": "No tienes acceso en ningún canal.", "You have no games. Use \u0002CHALLENGE \u0002.": "No tienes partidas. Usa \u0002CHALLENGE \u0002.", "You have no memo #\u0002{n}\u0002.": "No tienes el memo #\u0002{n}\u0002.", @@ -1258,7 +1235,6 @@ "your mailbox summary": "el resumen de tu buzón", "your memo preferences": "tus preferencias de memos", "{challenger} challenges you to {game} (game #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002": "{challenger} te desafía a {game} (partida #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002", - "{count} ticket(s). \u0002VIEW\u0002 for detail.": "{count} ticket(s). \u0002VIEW\u0002 para el detalle.", "{name} added for \u0002{mask}\u0002 (temporary).": "{name} añadido para \u0002{mask}\u0002 (temporal).", "{name} added for \u0002{mask}\u0002.": "{name} añadido para \u0002{mask}\u0002.", "{name} for \u0002{mask}\u0002 removed.": "{name} para \u0002{mask}\u0002 eliminado.", @@ -1266,7 +1242,6 @@ "{name} updated for \u0002{mask}\u0002.": "{name} actualizado para \u0002{mask}\u0002.", "{nick} declined your challenge #{id}.": "{nick} rechazó tu desafío #{id}.", "{num}. {text} — by {by}": "{num}. {text} — por {by}", - "{n} report(s). \u0002VIEW\u0002 for detail.": "{n} informe(s). \u0002VIEW\u0002 para el detalle.", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}) — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] por {setter} ({when}) — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}), expires in {ttl} — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] por {setter} ({when}), expira en {ttl} — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] — {reason}", @@ -1283,5 +1258,55 @@ "{who}: I have no record of \u0002{nick}\u0002 talking in \u0002{chan}\u0002.": "{who}: No tengo registro de que \u0002{nick}\u0002 haya hablado en \u0002{chan}\u0002.", "{word} list for \u0002{name}\u0002:": "lista de {word} para \u0002{name}\u0002:", "Type \u0002HELP \u0002 for detail on one command.": "Escribe \u0002HELP \u0002 para más detalle sobre un comando.", - "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "No hay ayuda para \u0002{t}\u0002. Escribe \u0002HELP\u0002 para la lista de comandos." + "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "No hay ayuda para \u0002{t}\u0002. Escribe \u0002HELP\u0002 para la lista de comandos.", + "Reopped \u0002{opped}\u0002 trusted regular in \u0002{chan}\u0002.": "Se volvió a dar op a \u0002{opped}\u0002 habitual de confianza en \u0002{chan}\u0002.", + "Reopped \u0002{opped}\u0002 trusted regulars in \u0002{chan}\u0002.": "Se volvió a dar op a \u0002{opped}\u0002 habituales de confianza en \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 trigger from \u0002{chan}\u0002.": "Se borró \u0002{n}\u0002 disparador de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 triggers from \u0002{chan}\u0002.": "Se borraron \u0002{n}\u0002 disparadores de \u0002{chan}\u0002.", + " Serving {count} channel: {list}": " Atendiendo {count} canal: {list}", + " Serving {count} channels: {list}": " Atendiendo {count} canales: {list}", + "Removed all \u0002{n}\u0002 bot.": "Se eliminó \u0002{n}\u0002 bot en total.", + "Removed all \u0002{n}\u0002 bots.": "Se eliminaron los \u0002{n}\u0002 bots.", + "Cleared \u0002{n}\u0002 badword pattern from \u0002{chan}\u0002.": "Se borró \u0002{n}\u0002 patrón de palabra prohibida de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 badword patterns from \u0002{chan}\u0002.": "Se borraron \u0002{n}\u0002 patrones de palabra prohibida de \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kick in \u0002{chan}\u0002.": "El bot vetará a un usuario después de \u0002{n}\u0002 expulsión en \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kicks in \u0002{chan}\u0002.": "El bot vetará a un usuario después de \u0002{n}\u0002 expulsiones en \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entry from \u0002{chan}\u0002's auto-kick list.": "Se borró \u0002{count}\u0002 entrada de la lista de auto-expulsión de \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entries from \u0002{chan}\u0002's auto-kick list.": "Se borraron \u0002{count}\u0002 entradas de la lista de auto-expulsión de \u0002{chan}\u0002.", + "Top {shown} of {total} scored identity for \u0002{chan}\u0002.": "Top {shown} de {total} identidad puntuada para \u0002{chan}\u0002.", + "Top {shown} of {total} scored identities for \u0002{chan}\u0002.": "Top {shown} de {total} identidades puntuadas para \u0002{chan}\u0002.", + "\u0002{n}\u0002 vote will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 voto llevará ahora un \u0002!votekick\u0002/\u0002!voteban\u0002 en \u0002{chan}\u0002.", + "\u0002{n}\u0002 votes will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 votos llevarán ahora un \u0002!votekick\u0002/\u0002!voteban\u0002 en \u0002{chan}\u0002.", + "End of flags ({count} entry).": "Fin de los flags ({count} entrada).", + "End of flags ({count} entries).": "Fin de los flags ({count} entradas).", + "Deleted all \u0002{count}\u0002 memo.": "Se eliminó \u0002{count}\u0002 memo en total.", + "Deleted all \u0002{count}\u0002 memos.": "Se eliminaron los \u0002{count}\u0002 memos.", + "Memo sent to \u0002{sent}\u0002 account.": "Memo enviado a \u0002{sent}\u0002 cuenta.", + "Memo sent to \u0002{sent}\u0002 accounts.": "Memo enviado a \u0002{sent}\u0002 cuentas.", + "End of session list ({count} IP).": "Fin de la lista de sesiones ({count} IP).", + "End of session list ({count} IPs).": "Fin de la lista de sesiones ({count} IPs).", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live session.": "\u0002{ip}\u0002 tiene \u0002{count}\u0002 sesión activa.", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live sessions.": "\u0002{ip}\u0002 tiene \u0002{count}\u0002 sesiones activas.", + "You have \u0002{total}\u0002 memo, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Tienes \u0002{total}\u0002 memo, \u0002{unread}\u0002 sin leer, de un máximo de \u0002{max}\u0002.", + "You have \u0002{total}\u0002 memos, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Tienes \u0002{total}\u0002 memos, \u0002{unread}\u0002 sin leer, de un máximo de \u0002{max}\u0002.", + "End of list — {count} match{more}.": "Fin de la lista — {count} coincidencia{more}.", + "End of list — {count} matches{more}.": "Fin de la lista — {count} coincidencias{more}.", + "Memo sent to \u0002{sent}\u0002 operator.": "Memo enviado a \u0002{sent}\u0002 operador.", + "Memo sent to \u0002{sent}\u0002 operators.": "Memo enviado a \u0002{sent}\u0002 operadores.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host AKILL'd.": "CHANKILL en \u0002{chan}\u0002: \u0002{banned}\u0002 host con AKILL.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 hosts AKILL'd.": "CHANKILL en \u0002{chan}\u0002: \u0002{banned}\u0002 hosts con AKILL.", + " Auto-join : {count} channel — see \u0002AJOIN LIST\u0002": " Auto-unión : {count} canal — mira \u0002AJOIN LIST\u0002", + " Auto-join : {count} channels — see \u0002AJOIN LIST\u0002": " Auto-unión : {count} canales — mira \u0002AJOIN LIST\u0002", + "{count} ticket. \u0002VIEW\u0002 for detail.": "{count} ticket. \u0002VIEW\u0002 para ver el detalle.", + "{count} tickets. \u0002VIEW\u0002 for detail.": "{count} tickets. \u0002VIEW\u0002 para ver el detalle.", + "You have \u0002{unread}\u0002 new memo. Read it with \u0002/msg MemoServ READ NEW\u0002.": "Tienes \u0002{unread}\u0002 memo nuevo. Léelo con \u0002/msg MemoServ READ NEW\u0002.", + "You have \u0002{unread}\u0002 new memos. Read them with \u0002/msg MemoServ READ NEW\u0002.": "Tienes \u0002{unread}\u0002 memos nuevos. Léelos con \u0002/msg MemoServ READ NEW\u0002.", + "Cleared \u0002{n}\u0002 notify watch.": "Se borró \u0002{n}\u0002 seguimiento de notificación.", + "Cleared \u0002{n}\u0002 notify watches.": "Se borraron \u0002{n}\u0002 seguimientos de notificación.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 line seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 línea vista.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 lines seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 líneas vistas.", + "{n} report. \u0002VIEW\u0002 for detail.": "{n} reporte. \u0002VIEW\u0002 para ver el detalle.", + "{n} reports. \u0002VIEW\u0002 for detail.": "{n} reportes. \u0002VIEW\u0002 para ver el detalle.", + "End of list ({count} group).": "Fin de la lista ({count} grupo).", + "End of list ({count} groups).": "Fin de la lista ({count} grupos)." } \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json index 8e92906..ee8757d 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -61,7 +61,6 @@ "\u0002{chan}\u0002 isn't suspended.": "\u0002{chan}\u0002 n'est pas suspendu.", "\u0002{chan}\u0002 was already set that way.": "\u0002{chan}\u0002 était déjà réglé ainsi.", "\u0002{chan}\u0002 will no longer expire.": "\u0002{chan}\u0002 n'expirera plus.", - "\u0002{chan}\u0002 — \u0002{lines}\u0002 line(s) seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 ligne(s) vue(s).", "\u0002{code}\u0002 isn't an available language. Available: {list}.": "\u0002{code}\u0002 n'est pas une langue disponible. Disponibles : {list}.", "\u0002{d}\u0002 isn't a valid expiry — try 30d, 12h, 45m, or 0.": "\u0002{d}\u0002 n'est pas une expiration valide — essayez 30d, 12h, 45m ou 0.", "\u0002{host}\u0002 is already in use. Please choose another.": "\u0002{host}\u0002 est déjà utilisé. Veuillez en choisir un autre.", @@ -69,7 +68,6 @@ "\u0002{host}\u0002 isn't a valid host (letters, digits, hyphens and dots).": "\u0002{host}\u0002 n'est pas un hôte valide (lettres, chiffres, tirets et points).", "\u0002{host}\u0002 isn't a valid host.": "\u0002{host}\u0002 n'est pas un hôte valide.", "\u0002{host}\u0002 isn't allowed here. Please choose another.": "\u0002{host}\u0002 n'est pas autorisé ici. Veuillez en choisir un autre.", - "\u0002{ip}\u0002 has \u0002{count}\u0002 live session(s).": "\u0002{ip}\u0002 a \u0002{count}\u0002 session(s) active(s).", "\u0002{label}\u0002 access on \u0002{chan}\u0002: \u0002{level}\u0002": "Accès de \u0002{label}\u0002 sur \u0002{chan}\u0002 : \u0002{level}\u0002", "\u0002{mask}\u0002 isn't on \u0002{chan}\u0002's auto-kick list.": "\u0002{mask}\u0002 n'est pas dans la liste d'auto-kick de \u0002{chan}\u0002.", "\u0002{mask}\u0002 isn't on the ignore list.": "\u0002{mask}\u0002 n'est pas dans la liste d'ignore.", @@ -88,7 +86,6 @@ "\u0002{nick}\u0002 isn't grouped to your account.": "\u0002{nick}\u0002 n'est pas groupé à votre compte.", "\u0002{nick}\u0002 isn't here.": "\u0002{nick}\u0002 n'est pas ici.", "\u0002{nick}\u0002 was last seen {when} ({what}).": "\u0002{nick}\u0002 a été vu pour la dernière fois {when} ({what}).", - "\u0002{n}\u0002 vote(s) will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 vote(s) suffiront désormais pour un \u0002!votekick\u0002/\u0002!voteban\u0002 dans \u0002{chan}\u0002.", "\u0002{pattern}\u0002 isn't a valid regular expression.": "\u0002{pattern}\u0002 n'est pas une expression régulière valide.", "\u0002{raw}\u0002 isn't a valid \u0002{target}\u0002 mask.": "\u0002{raw}\u0002 n'est pas un masque \u0002{target}\u0002 valide.", "\u0002{target}\u0002 had no access to \u0002{chan}\u0002.": "\u0002{target}\u0002 n'avait aucun accès à \u0002{chan}\u0002.", @@ -143,7 +140,6 @@ " (custom)": " (personnalisé)", " (no activity counters yet)": " (aucun compteur d'activité pour l'instant)", " About : \u0002{target}\u0002": " Sujet : \u0002{target}\u0002", - " Auto-join : {count} channel(s) — see \u0002AJOIN LIST\u0002": " Auto-join : {count} salon(s) — voir \u0002AJOIN LIST\u0002", " By : \u0002{by}\u0002": " Par : \u0002{by}\u0002", " Description: {desc}": " Description: {desc}", " Email : (none set)": " Email : (aucun défini)", @@ -165,7 +161,6 @@ " Real name: {gecos}": " Nom réel : {gecos}", " Reason : {reason}": " Raison : {reason}", " Registered : {when}": " Enregistré : {when}", - " Serving {count} channel(s): {list}": " Sert {count} salon(s) : {list}", " Staff note : {note}": " Note du staff : {note}", " Suspended : by \u0002{by}\u0002 — {reason}": " Suspendu : par \u0002{by}\u0002 — {reason}", " URL : {url}": " URL : {url}", @@ -278,7 +273,6 @@ "Both channels must be registered.": "Les deux salons doivent être enregistrés.", "Bots ({count}):": "Bots ({count}) :", "CALC and show each die": "CALC en affichant chaque dé", - "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) AKILL'd.": "CHANKILL sur \u0002{chan}\u0002 : \u0002{banned}\u0002 hôte(s) AKILL.", "Can't activate: \u0002{host}\u0002 is on the forbidden list.": "Impossible d'activer : \u0002{host}\u0002 figure sur la liste des interdits.", "Can't activate: {msg}": "Impossible d'activer : {msg}", "Challenge \u0002#{id}\u0002 ({game}) sent to \u0002{target}\u0002.": "Défi \u0002#{id}\u0002 ({game}) envoyé à \u0002{target}\u0002.", @@ -291,10 +285,6 @@ "Channels released: {list}.": "Salons libérés : {list}.", "Channels you have access on ({count}):": "Salons sur lesquels vous avez un accès ({count}) :", "Classement \u0002{game}\u0002 vide. Sois le premier à gagner !": "Classement \u0002{game}\u0002 vide. Soyez le premier à gagner !", - "Cleared \u0002{count}\u0002 entr{suffix} from \u0002{chan}\u0002's auto-kick list.": "Suppression de \u0002{count}\u0002 entré{suffix} de la liste d'auto-kick de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 badword pattern(s) from \u0002{chan}\u0002.": "\u0002{n}\u0002 motif(s) de gros mots effacé(s) de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 notify watch(es).": "\u0002{n}\u0002 surveillance(s) effacée(s).", - "Cleared \u0002{n}\u0002 trigger(s) from \u0002{chan}\u0002.": "\u0002{n}\u0002 déclencheur(s) effacé(s) de \u0002{chan}\u0002.", "Cleared \u0002{target}\u0002's access to \u0002{chan}\u0002.": "Accès de \u0002{target}\u0002 à \u0002{chan}\u0002 effacé.", "Cleared the *!*@{host} ban on \u0002{chan}\u0002.": "Ban *!*@{host} retiré sur \u0002{chan}\u0002.", "Contact email for \u0002{chan}\u0002 cleared.": "E-mail de contact de \u0002{chan}\u0002 effacé.", @@ -306,22 +296,17 @@ "Couldn't update \u0002{chan}\u0002 — please try again in a moment.": "Impossible de mettre à jour \u0002{chan}\u0002 — veuillez réessayer dans un instant.", "DEFCON set to \u0002{level}\u0002 — {desc}.": "DEFCON réglé sur \u0002{level}\u0002 — {desc}.", "Declined.": "Refusé.", - "Deleted all \u0002{count}\u0002 memo(s).": "Tous les \u0002{count}\u0002 mémo(s) ont été supprimés.", "Description for \u0002{chan}\u0002 updated.": "Description de \u0002{chan}\u0002 mise à jour.", "DiceServ rolls dice and evaluates maths for tabletop games. Dice: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Also + - * / ^ %, parentheses, functions (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, and \u00023~2d6\u0002 to repeat.": "DiceServ lance des dés et évalue des calculs pour les jeux de rôle sur table. Dés : \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Aussi + - * / ^ %, parenthèses, fonctions (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, et \u00023~2d6\u0002 pour répéter.", "DictServ looks words up in the dict.org dictionaries. Try \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (thesaurus), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, and more — \u0002LOOKUP\u0002 lists them all.": "DictServ recherche des mots dans les dictionnaires de dict.org. Essayez \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (thésaurus), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, et plus — \u0002LOOKUP\u0002 les liste tous.", "Email for \u0002{account}\u0002 cleared.": "Email de \u0002{account}\u0002 effacé.", "Email for \u0002{account}\u0002 updated.": "Email de \u0002{account}\u0002 mis à jour.", "End of exception list ({count} shown).": "Fin de la liste des exceptions ({count} affiché(s)).", - "End of flags ({count} entr{suffix}).": "Fin des flags ({count} entré{suffix}).", "End of ignore list ({count} shown).": "Fin de la liste d'ignore ({count} affiché(s)).", "End of jupe list ({count} shown).": "Fin de la liste des jupes ({count} affiché(s)).", - "End of list ({count} group(s)).": "Fin de la liste ({count} groupe(s)).", - "End of list — {count} match(es){more}.": "Fin de la liste — {count} correspondance(s){more}.", "End of notify list ({shown} shown).": "Fin de la liste de surveillance ({shown} affiché(s)).", "End of results ({count} shown, newest first — refine the search to narrow).": "Fin des résultats ({count} affiché(s), du plus récent au plus ancien — affinez la recherche pour restreindre).", "End of runtime operator list ({count} shown).": "Fin de la liste des opérateurs d'exécution ({count} affiché(s)).", - "End of session list ({count} IP(s)).": "Fin de la liste des sessions ({count} IP).", "End of spam-filter list ({shown} shown).": "Fin de la liste des filtres anti-spam ({shown} affiché(s)).", "End of {name} list ({shown} shown).": "Fin de la liste {name} ({shown} affiché(s)).", "End of {which} bulletins ({count} shown).": "Fin des bulletins {which} ({count} affiché(s)).", @@ -387,8 +372,6 @@ "Mask must be a \u0002#channel\u0002, a \u0002nick!user@host\u0002 pattern, or a bare nick glob — and no spaces.": "Le masque doit être un \u0002#canal\u0002, un motif \u0002nick!user@host\u0002, ou un simple glob de pseudo — et sans espaces.", "Memo #\u0002{num}\u0002 from \u0002{from}\u0002 ({when}):": "Mémo n°\u0002{num}\u0002 de \u0002{from}\u0002 ({when}) :", "Memo #\u0002{n}\u0002 deleted.": "Mémo n°\u0002{n}\u0002 supprimé.", - "Memo sent to \u0002{sent}\u0002 account(s).": "Mémo envoyé à \u0002{sent}\u0002 compte(s).", - "Memo sent to \u0002{sent}\u0002 operator(s).": "Mémo envoyé à \u0002{sent}\u0002 opérateur(s).", "Memo sent to \u0002{target}\u0002.": "Mémo envoyé à \u0002{target}\u0002.", "MemoServ delivers messages to registered users, online or not. You must be identified to use it.": "MemoServ délivre des messages aux utilisateurs enregistrés, connectés ou non. Vous devez être identifié pour l'utiliser.", "Mode lock for \u0002{chan}\u0002 updated.": "Verrou de modes de \u0002{chan}\u0002 mis à jour.", @@ -485,12 +468,10 @@ "Removed \u0002{mask}\u0002 from \u0002{chan}\u0002's auto-kick list.": "\u0002{mask}\u0002 a été retiré de la liste d'auto-kick de \u0002{chan}\u0002.", "Removed \u0002{target}\u0002 from \u0002{name}\u0002.": "\u0002{target}\u0002 a été retiré de \u0002{name}\u0002.", "Removed \u0002{which}\u0002 bulletin \u0002{n}\u0002.": "Bulletin \u0002{which}\u0002 \u0002{n}\u0002 supprimé.", - "Removed all \u0002{n}\u0002 bot(s).": "Tous les \u0002{n}\u0002 bot(s) supprimé(s).", "Removed badword pattern from \u0002{chan}\u0002.": "Motif de gros mot retiré de \u0002{chan}\u0002.", "Removed fingerprint \u0002{fp}\u0002.": "Empreinte \u0002{fp}\u0002 retirée.", "Removed forbidden pattern: {pattern}": "Motif interdit retiré : {pattern}", "Removed the forbid on {label} \u0002{mask}\u0002.": "Interdiction retirée pour {label} \u0002{mask}\u0002.", - "Reopped \u0002{opped}\u0002 trusted regular(s) in \u0002{chan}\u0002.": "\u0002{opped}\u0002 habitué(s) de confiance réoppé(s) dans \u0002{chan}\u0002.", "Report \u0002#{id}\u0002 ({state}):": "Signalement \u0002#{id}\u0002 ({state}) :", "Report \u0002#{n}\u0002 closed.": "Signalement \u0002#{n}\u0002 fermé.", "Report \u0002#{n}\u0002 deleted.": "Signalement \u0002#{n}\u0002 supprimé.", @@ -881,7 +862,6 @@ "The bot has left \u0002{chan}\u0002.": "Le bot a quitté \u0002{chan}\u0002.", "The bot may again kick channel operators in \u0002{chan}\u0002.": "Le bot peut à nouveau expulser les opérateurs du salon dans \u0002{chan}\u0002.", "The bot may again kick voiced users in \u0002{chan}\u0002.": "Le bot peut à nouveau expulser les utilisateurs voicés dans \u0002{chan}\u0002.", - "The bot will ban a user after \u0002{n}\u0002 kick(s) in \u0002{chan}\u0002.": "Le bot bannira un utilisateur après \u0002{n}\u0002 expulsion(s) dans \u0002{chan}\u0002.", "The bot will kick without warning in \u0002{chan}\u0002.": "Le bot expulsera sans avertissement dans \u0002{chan}\u0002.", "The bot will no longer kick channel operators in \u0002{chan}\u0002.": "Le bot n'expulsera plus les opérateurs du salon dans \u0002{chan}\u0002.", "The bot will no longer kick voiced users in \u0002{chan}\u0002.": "Le bot n'expulsera plus les utilisateurs voicés dans \u0002{chan}\u0002.", @@ -927,7 +907,6 @@ "Ticket \u0002#{id}\u0002 isn't open (or doesn't exist).": "Le ticket \u0002#{id}\u0002 n'est pas ouvert (ou n'existe pas).", "Too many failed attempts. Please wait {secs}s and try again.": "Trop de tentatives échouées. Veuillez patienter {secs}s et réessayer.", "Top talkers:": "Plus bavards :", - "Top {shown} of {total} scored identit{suffix} for \u0002{chan}\u0002.": "Top {shown} sur {total} identités classées pour \u0002{chan}\u0002.", "Topic for \u0002{chan}\u0002 updated.": "Sujet de \u0002{chan}\u0002 mis à jour.", "Trigger #\u0002{n}\u0002 removed from \u0002{chan}\u0002.": "Déclencheur #\u0002{n}\u0002 retiré de \u0002{chan}\u0002.", "Triggers for \u0002{chan}\u0002 ({count}):": "Déclencheurs pour \u0002{chan}\u0002 ({count}) :", @@ -958,8 +937,6 @@ "You can't ungroup your main account name.": "Vous ne pouvez pas dégrouper le nom principal de votre compte.", "You cannot challenge yourself.": "Vous ne pouvez pas vous défier vous-même.", "You don't have an account to confirm.": "Vous n'avez pas de compte à confirmer.", - "You have \u0002{total}\u0002 memo(s), \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Vous avez \u0002{total}\u0002 mémo(s), \u0002{unread}\u0002 non lu(s), sur un maximum de \u0002{max}\u0002.", - "You have \u0002{unread}\u0002 new memo(s). Read them with \u0002/msg MemoServ READ NEW\u0002.": "Vous avez \u0002{unread}\u0002 nouveau(x) mémo(s). Lisez-les avec \u0002/msg MemoServ READ NEW\u0002.", "You have access on no channels.": "Vous n'avez d'accès sur aucun salon.", "You have no games. Use \u0002CHALLENGE \u0002.": "Vous n'avez aucune partie. Utilisez \u0002CHALLENGE \u0002.", "You have no memo #\u0002{n}\u0002.": "Vous n'avez aucun mémo n°\u0002{n}\u0002.", @@ -1258,7 +1235,6 @@ "your mailbox summary": "résumé de votre boîte", "your memo preferences": "vos préférences de mémos", "{challenger} challenges you to {game} (game #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002": "{challenger} vous défie au {game} (partie #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002", - "{count} ticket(s). \u0002VIEW\u0002 for detail.": "{count} ticket(s). \u0002VIEW\u0002 pour le détail.", "{name} added for \u0002{mask}\u0002 (temporary).": "{name} ajouté pour \u0002{mask}\u0002 (temporaire).", "{name} added for \u0002{mask}\u0002.": "{name} ajouté pour \u0002{mask}\u0002.", "{name} for \u0002{mask}\u0002 removed.": "{name} pour \u0002{mask}\u0002 supprimé.", @@ -1266,7 +1242,6 @@ "{name} updated for \u0002{mask}\u0002.": "{name} mis à jour pour \u0002{mask}\u0002.", "{nick} declined your challenge #{id}.": "{nick} a refusé votre défi #{id}.", "{num}. {text} — by {by}": "{num}. {text} — par {by}", - "{n} report(s). \u0002VIEW\u0002 for detail.": "{n} signalement(s). \u0002VIEW\u0002 pour le détail.", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}) — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] par {setter} ({when}) — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}), expires in {ttl} — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] par {setter} ({when}), expire dans {ttl} — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] — {reason}", @@ -1283,5 +1258,55 @@ "{who}: I have no record of \u0002{nick}\u0002 talking in \u0002{chan}\u0002.": "{who} : je n'ai aucune trace de \u0002{nick}\u0002 ayant parlé dans \u0002{chan}\u0002.", "{word} list for \u0002{name}\u0002:": "Liste {word} pour \u0002{name}\u0002 :", "Type \u0002HELP \u0002 for detail on one command.": "Tapez \u0002HELP \u0002 pour le détail d'une commande.", - "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "Aucune aide pour \u0002{t}\u0002. Tapez \u0002HELP\u0002 pour la liste des commandes." + "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "Aucune aide pour \u0002{t}\u0002. Tapez \u0002HELP\u0002 pour la liste des commandes.", + "Reopped \u0002{opped}\u0002 trusted regular in \u0002{chan}\u0002.": "\u0002{opped}\u0002 habitué de confiance réoppé dans \u0002{chan}\u0002.", + "Reopped \u0002{opped}\u0002 trusted regulars in \u0002{chan}\u0002.": "\u0002{opped}\u0002 habitués de confiance réoppés dans \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 trigger from \u0002{chan}\u0002.": "\u0002{n}\u0002 déclencheur supprimé de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 triggers from \u0002{chan}\u0002.": "\u0002{n}\u0002 déclencheurs supprimés de \u0002{chan}\u0002.", + " Serving {count} channel: {list}": " Dessert {count} salon : {list}", + " Serving {count} channels: {list}": " Dessert {count} salons : {list}", + "Removed all \u0002{n}\u0002 bot.": "\u0002{n}\u0002 bot supprimé (tous).", + "Removed all \u0002{n}\u0002 bots.": "Tous les \u0002{n}\u0002 bots supprimés.", + "Cleared \u0002{n}\u0002 badword pattern from \u0002{chan}\u0002.": "\u0002{n}\u0002 motif d'insulte supprimé de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 badword patterns from \u0002{chan}\u0002.": "\u0002{n}\u0002 motifs d'insulte supprimés de \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kick in \u0002{chan}\u0002.": "Le bot bannira un utilisateur après \u0002{n}\u0002 kick dans \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kicks in \u0002{chan}\u0002.": "Le bot bannira un utilisateur après \u0002{n}\u0002 kicks dans \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entry from \u0002{chan}\u0002's auto-kick list.": "\u0002{count}\u0002 entrée supprimée de la liste d'auto-kick de \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entries from \u0002{chan}\u0002's auto-kick list.": "\u0002{count}\u0002 entrées supprimées de la liste d'auto-kick de \u0002{chan}\u0002.", + "Top {shown} of {total} scored identity for \u0002{chan}\u0002.": "Top {shown} sur {total} identité notée pour \u0002{chan}\u0002.", + "Top {shown} of {total} scored identities for \u0002{chan}\u0002.": "Top {shown} sur {total} identités notées pour \u0002{chan}\u0002.", + "\u0002{n}\u0002 vote will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 vote fera désormais aboutir un \u0002!votekick\u0002/\u0002!voteban\u0002 dans \u0002{chan}\u0002.", + "\u0002{n}\u0002 votes will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 votes feront désormais aboutir un \u0002!votekick\u0002/\u0002!voteban\u0002 dans \u0002{chan}\u0002.", + "End of flags ({count} entry).": "Fin des flags ({count} entrée).", + "End of flags ({count} entries).": "Fin des flags ({count} entrées).", + "Deleted all \u0002{count}\u0002 memo.": "\u0002{count}\u0002 mémo supprimé (tous).", + "Deleted all \u0002{count}\u0002 memos.": "Tous les \u0002{count}\u0002 mémos supprimés.", + "Memo sent to \u0002{sent}\u0002 account.": "Mémo envoyé à \u0002{sent}\u0002 compte.", + "Memo sent to \u0002{sent}\u0002 accounts.": "Mémo envoyé à \u0002{sent}\u0002 comptes.", + "End of session list ({count} IP).": "Fin de la liste des sessions ({count} IP).", + "End of session list ({count} IPs).": "Fin de la liste des sessions ({count} IP).", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live session.": "\u0002{ip}\u0002 a \u0002{count}\u0002 session active.", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live sessions.": "\u0002{ip}\u0002 a \u0002{count}\u0002 sessions actives.", + "You have \u0002{total}\u0002 memo, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Vous avez \u0002{total}\u0002 mémo, \u0002{unread}\u0002 non lu, sur un maximum de \u0002{max}\u0002.", + "You have \u0002{total}\u0002 memos, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Vous avez \u0002{total}\u0002 mémos, \u0002{unread}\u0002 non lus, sur un maximum de \u0002{max}\u0002.", + "End of list — {count} match{more}.": "Fin de la liste — {count} correspondance{more}.", + "End of list — {count} matches{more}.": "Fin de la liste — {count} correspondances{more}.", + "Memo sent to \u0002{sent}\u0002 operator.": "Mémo envoyé à \u0002{sent}\u0002 opérateur.", + "Memo sent to \u0002{sent}\u0002 operators.": "Mémo envoyé à \u0002{sent}\u0002 opérateurs.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host AKILL'd.": "CHANKILL sur \u0002{chan}\u0002 : \u0002{banned}\u0002 hôte AKILL'd.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 hosts AKILL'd.": "CHANKILL sur \u0002{chan}\u0002 : \u0002{banned}\u0002 hôtes AKILL'd.", + " Auto-join : {count} channel — see \u0002AJOIN LIST\u0002": " Auto-join : {count} salon — voir \u0002AJOIN LIST\u0002", + " Auto-join : {count} channels — see \u0002AJOIN LIST\u0002": " Auto-join : {count} salons — voir \u0002AJOIN LIST\u0002", + "{count} ticket. \u0002VIEW\u0002 for detail.": "{count} ticket. \u0002VIEW\u0002 pour le détail.", + "{count} tickets. \u0002VIEW\u0002 for detail.": "{count} tickets. \u0002VIEW\u0002 pour le détail.", + "You have \u0002{unread}\u0002 new memo. Read it with \u0002/msg MemoServ READ NEW\u0002.": "Vous avez \u0002{unread}\u0002 nouveau mémo. Lisez-le avec \u0002/msg MemoServ READ NEW\u0002.", + "You have \u0002{unread}\u0002 new memos. Read them with \u0002/msg MemoServ READ NEW\u0002.": "Vous avez \u0002{unread}\u0002 nouveaux mémos. Lisez-les avec \u0002/msg MemoServ READ NEW\u0002.", + "Cleared \u0002{n}\u0002 notify watch.": "\u0002{n}\u0002 surveillance notify supprimée.", + "Cleared \u0002{n}\u0002 notify watches.": "\u0002{n}\u0002 surveillances notify supprimées.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 line seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 ligne vue.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 lines seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 lignes vues.", + "{n} report. \u0002VIEW\u0002 for detail.": "{n} signalement. \u0002VIEW\u0002 pour le détail.", + "{n} reports. \u0002VIEW\u0002 for detail.": "{n} signalements. \u0002VIEW\u0002 pour le détail.", + "End of list ({count} group).": "Fin de la liste ({count} groupe).", + "End of list ({count} groups).": "Fin de la liste ({count} groupes)." } \ No newline at end of file diff --git a/lang/pt-br.json b/lang/pt-br.json index b100078..da2e12e 100644 --- a/lang/pt-br.json +++ b/lang/pt-br.json @@ -61,7 +61,6 @@ "\u0002{chan}\u0002 isn't suspended.": "\u0002{chan}\u0002 não está suspenso.", "\u0002{chan}\u0002 was already set that way.": "\u0002{chan}\u0002 já estava definido assim.", "\u0002{chan}\u0002 will no longer expire.": "\u0002{chan}\u0002 não expirará mais.", - "\u0002{chan}\u0002 — \u0002{lines}\u0002 line(s) seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 linha(s) vista(s).", "\u0002{code}\u0002 isn't an available language. Available: {list}.": "\u0002{code}\u0002 não é um idioma disponível. Disponíveis: {list}.", "\u0002{d}\u0002 isn't a valid expiry — try 30d, 12h, 45m, or 0.": "\u0002{d}\u0002 não é uma expiração válida — tente 30d, 12h, 45m ou 0.", "\u0002{host}\u0002 is already in use. Please choose another.": "\u0002{host}\u0002 já está em uso. Por favor, escolha outro.", @@ -69,7 +68,6 @@ "\u0002{host}\u0002 isn't a valid host (letters, digits, hyphens and dots).": "\u0002{host}\u0002 não é um host válido (letras, dígitos, hifens e pontos).", "\u0002{host}\u0002 isn't a valid host.": "\u0002{host}\u0002 não é um host válido.", "\u0002{host}\u0002 isn't allowed here. Please choose another.": "\u0002{host}\u0002 não é permitido aqui. Por favor, escolha outro.", - "\u0002{ip}\u0002 has \u0002{count}\u0002 live session(s).": "\u0002{ip}\u0002 tem \u0002{count}\u0002 sessão(ões) ativa(s).", "\u0002{label}\u0002 access on \u0002{chan}\u0002: \u0002{level}\u0002": "\u0002{label}\u0002 acesso em \u0002{chan}\u0002: \u0002{level}\u0002", "\u0002{mask}\u0002 isn't on \u0002{chan}\u0002's auto-kick list.": "\u0002{mask}\u0002 não está na lista de auto-kick de \u0002{chan}\u0002.", "\u0002{mask}\u0002 isn't on the ignore list.": "\u0002{mask}\u0002 não está na lista de ignorados.", @@ -88,7 +86,6 @@ "\u0002{nick}\u0002 isn't grouped to your account.": "\u0002{nick}\u0002 não está agrupado à sua conta.", "\u0002{nick}\u0002 isn't here.": "\u0002{nick}\u0002 não está aqui.", "\u0002{nick}\u0002 was last seen {when} ({what}).": "\u0002{nick}\u0002 foi visto pela última vez {when} ({what}).", - "\u0002{n}\u0002 vote(s) will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 voto(s) agora acionarão um \u0002!votekick\u0002/\u0002!voteban\u0002 em \u0002{chan}\u0002.", "\u0002{pattern}\u0002 isn't a valid regular expression.": "\u0002{pattern}\u0002 não é uma expressão regular válida.", "\u0002{raw}\u0002 isn't a valid \u0002{target}\u0002 mask.": "\u0002{raw}\u0002 não é uma máscara \u0002{target}\u0002 válida.", "\u0002{target}\u0002 had no access to \u0002{chan}\u0002.": "\u0002{target}\u0002 não tinha acesso a \u0002{chan}\u0002.", @@ -143,7 +140,6 @@ " (custom)": " (personalizado)", " (no activity counters yet)": " (ainda sem contadores de atividade)", " About : \u0002{target}\u0002": " Sobre : \u0002{target}\u0002", - " Auto-join : {count} channel(s) — see \u0002AJOIN LIST\u0002": " Auto-join : {count} canal(is) — veja \u0002AJOIN LIST\u0002", " By : \u0002{by}\u0002": " Por : \u0002{by}\u0002", " Description: {desc}": " Descrição: {desc}", " Email : (none set)": " Email : (nenhum definido)", @@ -165,7 +161,6 @@ " Real name: {gecos}": " Nome real: {gecos}", " Reason : {reason}": " Motivo : {reason}", " Registered : {when}": " Registrado : {when}", - " Serving {count} channel(s): {list}": " Atendendo {count} canal(is): {list}", " Staff note : {note}": " Nota da equipe : {note}", " Suspended : by \u0002{by}\u0002 — {reason}": " Suspenso : por \u0002{by}\u0002 — {reason}", " URL : {url}": " URL : {url}", @@ -278,7 +273,6 @@ "Both channels must be registered.": "Ambos os canais precisam estar registrados.", "Bots ({count}):": "Bots ({count}):", "CALC and show each die": "CALC e mostrar cada dado", - "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) AKILL'd.": "CHANKILL em \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) sob AKILL.", "Can't activate: \u0002{host}\u0002 is on the forbidden list.": "Não é possível ativar: \u0002{host}\u0002 está na lista de proibidos.", "Can't activate: {msg}": "Não é possível ativar: {msg}", "Challenge \u0002#{id}\u0002 ({game}) sent to \u0002{target}\u0002.": "Desafio \u0002#{id}\u0002 ({game}) enviado a \u0002{target}\u0002.", @@ -291,10 +285,6 @@ "Channels released: {list}.": "Canais liberados: {list}.", "Channels you have access on ({count}):": "Canais em que você tem acesso ({count}):", "Classement \u0002{game}\u0002 vide. Sois le premier à gagner !": "Classificação de \u0002{game}\u0002 vazia. Seja o primeiro a vencer!", - "Cleared \u0002{count}\u0002 entr{suffix} from \u0002{chan}\u0002's auto-kick list.": "\u0002{count}\u0002 entrada{suffix} removida(s) da lista de auto-kick de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 badword pattern(s) from \u0002{chan}\u0002.": "\u0002{n}\u0002 padrão(ões) de palavrão removido(s) de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 notify watch(es).": "\u0002{n}\u0002 observação(ões) de notify removida(s).", - "Cleared \u0002{n}\u0002 trigger(s) from \u0002{chan}\u0002.": "\u0002{n}\u0002 gatilho(s) removido(s) de \u0002{chan}\u0002.", "Cleared \u0002{target}\u0002's access to \u0002{chan}\u0002.": "Acesso de \u0002{target}\u0002 a \u0002{chan}\u0002 removido.", "Cleared the *!*@{host} ban on \u0002{chan}\u0002.": "Banimento *!*@{host} removido de \u0002{chan}\u0002.", "Contact email for \u0002{chan}\u0002 cleared.": "Email de contato para \u0002{chan}\u0002 apagado.", @@ -306,22 +296,17 @@ "Couldn't update \u0002{chan}\u0002 — please try again in a moment.": "Não foi possível atualizar \u0002{chan}\u0002 — por favor tente novamente em um instante.", "DEFCON set to \u0002{level}\u0002 — {desc}.": "DEFCON definido como \u0002{level}\u0002 — {desc}.", "Declined.": "Recusado.", - "Deleted all \u0002{count}\u0002 memo(s).": "Todos os \u0002{count}\u0002 memorando(s) excluídos.", "Description for \u0002{chan}\u0002 updated.": "Descrição para \u0002{chan}\u0002 atualizada.", "DiceServ rolls dice and evaluates maths for tabletop games. Dice: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Also + - * / ^ %, parentheses, functions (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, and \u00023~2d6\u0002 to repeat.": "O DiceServ rola dados e avalia contas para jogos de mesa. Dados: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Também + - * / ^ %, parênteses, funções (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, e \u00023~2d6\u0002 para repetir.", "DictServ looks words up in the dict.org dictionaries. Try \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (thesaurus), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, and more — \u0002LOOKUP\u0002 lists them all.": "O DictServ procura palavras nos dicionários do dict.org. Tente \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (dicionário de sinônimos), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, e mais — \u0002LOOKUP\u0002 lista todos.", "Email for \u0002{account}\u0002 cleared.": "Email de \u0002{account}\u0002 apagado.", "Email for \u0002{account}\u0002 updated.": "Email de \u0002{account}\u0002 atualizado.", "End of exception list ({count} shown).": "Fim da lista de exceções ({count} mostrada(s)).", - "End of flags ({count} entr{suffix}).": "Fim das flags ({count} entrada{suffix}).", "End of ignore list ({count} shown).": "Fim da lista de ignorados ({count} mostrada(s)).", "End of jupe list ({count} shown).": "Fim da lista de jupes ({count} mostrada(s)).", - "End of list ({count} group(s)).": "Fim da lista ({count} grupo(s)).", - "End of list — {count} match(es){more}.": "Fim da lista — {count} correspondência(s){more}.", "End of notify list ({shown} shown).": "Fim da lista de notify ({shown} mostrada(s)).", "End of results ({count} shown, newest first — refine the search to narrow).": "Fim dos resultados ({count} mostrado(s), mais recentes primeiro — refine a busca para restringir).", "End of runtime operator list ({count} shown).": "Fim da lista de operadores em tempo de execução ({count} mostrado(s)).", - "End of session list ({count} IP(s)).": "Fim da lista de sessões ({count} IP(s)).", "End of spam-filter list ({shown} shown).": "Fim da lista de filtros de spam ({shown} mostrado(s)).", "End of {name} list ({shown} shown).": "Fim da lista {name} ({shown} mostrado(s)).", "End of {which} bulletins ({count} shown).": "Fim dos boletins {which} ({count} mostrado(s)).", @@ -387,8 +372,6 @@ "Mask must be a \u0002#channel\u0002, a \u0002nick!user@host\u0002 pattern, or a bare nick glob — and no spaces.": "A máscara deve ser um \u0002#channel\u0002, um padrão \u0002nick!user@host\u0002 ou um glob de apelido simples — e sem espaços.", "Memo #\u0002{num}\u0002 from \u0002{from}\u0002 ({when}):": "Memorando #\u0002{num}\u0002 de \u0002{from}\u0002 ({when}):", "Memo #\u0002{n}\u0002 deleted.": "Memorando #\u0002{n}\u0002 excluído.", - "Memo sent to \u0002{sent}\u0002 account(s).": "Memorando enviado a \u0002{sent}\u0002 conta(s).", - "Memo sent to \u0002{sent}\u0002 operator(s).": "Memorando enviado a \u0002{sent}\u0002 operador(es).", "Memo sent to \u0002{target}\u0002.": "Memorando enviado a \u0002{target}\u0002.", "MemoServ delivers messages to registered users, online or not. You must be identified to use it.": "O MemoServ entrega mensagens a usuários registrados, online ou não. Você precisa estar identificado para usá-lo.", "Mode lock for \u0002{chan}\u0002 updated.": "Trava de modos para \u0002{chan}\u0002 atualizada.", @@ -485,12 +468,10 @@ "Removed \u0002{mask}\u0002 from \u0002{chan}\u0002's auto-kick list.": "Removido \u0002{mask}\u0002 da lista de expulsão automática de \u0002{chan}\u0002.", "Removed \u0002{target}\u0002 from \u0002{name}\u0002.": "Removido \u0002{target}\u0002 de \u0002{name}\u0002.", "Removed \u0002{which}\u0002 bulletin \u0002{n}\u0002.": "Removido o boletim \u0002{which}\u0002 \u0002{n}\u0002.", - "Removed all \u0002{n}\u0002 bot(s).": "Removidos todos os \u0002{n}\u0002 bot(s).", "Removed badword pattern from \u0002{chan}\u0002.": "Removido o padrão de palavra proibida de \u0002{chan}\u0002.", "Removed fingerprint \u0002{fp}\u0002.": "Removida a impressão digital \u0002{fp}\u0002.", "Removed forbidden pattern: {pattern}": "Removido o padrão proibido: {pattern}", "Removed the forbid on {label} \u0002{mask}\u0002.": "Removida a proibição sobre {label} \u0002{mask}\u0002.", - "Reopped \u0002{opped}\u0002 trusted regular(s) in \u0002{chan}\u0002.": "Reopados \u0002{opped}\u0002 frequentador(es) confiável(is) em \u0002{chan}\u0002.", "Report \u0002#{id}\u0002 ({state}):": "Denúncia \u0002#{id}\u0002 ({state}):", "Report \u0002#{n}\u0002 closed.": "Denúncia \u0002#{n}\u0002 encerrada.", "Report \u0002#{n}\u0002 deleted.": "Denúncia \u0002#{n}\u0002 excluída.", @@ -881,7 +862,6 @@ "The bot has left \u0002{chan}\u0002.": "O bot saiu de \u0002{chan}\u0002.", "The bot may again kick channel operators in \u0002{chan}\u0002.": "O bot pode novamente expulsar operadores de canal em \u0002{chan}\u0002.", "The bot may again kick voiced users in \u0002{chan}\u0002.": "O bot pode novamente expulsar usuários com voz em \u0002{chan}\u0002.", - "The bot will ban a user after \u0002{n}\u0002 kick(s) in \u0002{chan}\u0002.": "O bot vai banir um usuário após \u0002{n}\u0002 expulsão(ões) em \u0002{chan}\u0002.", "The bot will kick without warning in \u0002{chan}\u0002.": "O bot vai expulsar sem aviso em \u0002{chan}\u0002.", "The bot will no longer kick channel operators in \u0002{chan}\u0002.": "O bot não vai mais expulsar operadores de canal em \u0002{chan}\u0002.", "The bot will no longer kick voiced users in \u0002{chan}\u0002.": "O bot não vai mais expulsar usuários com voz em \u0002{chan}\u0002.", @@ -927,7 +907,6 @@ "Ticket \u0002#{id}\u0002 isn't open (or doesn't exist).": "O ticket \u0002#{id}\u0002 não está aberto (ou não existe).", "Too many failed attempts. Please wait {secs}s and try again.": "Muitas tentativas malsucedidas. Aguarde {secs}s e tente novamente.", "Top talkers:": "Quem mais fala:", - "Top {shown} of {total} scored identit{suffix} for \u0002{chan}\u0002.": "Top {shown} de {total} identidade{suffix} pontuada(s) para \u0002{chan}\u0002.", "Topic for \u0002{chan}\u0002 updated.": "Tópico de \u0002{chan}\u0002 atualizado.", "Trigger #\u0002{n}\u0002 removed from \u0002{chan}\u0002.": "Gatilho #\u0002{n}\u0002 removido de \u0002{chan}\u0002.", "Triggers for \u0002{chan}\u0002 ({count}):": "Gatilhos de \u0002{chan}\u0002 ({count}):", @@ -958,8 +937,6 @@ "You can't ungroup your main account name.": "Você não pode desagrupar o nome principal da sua conta.", "You cannot challenge yourself.": "Você não pode desafiar a si mesmo.", "You don't have an account to confirm.": "Você não tem uma conta para confirmar.", - "You have \u0002{total}\u0002 memo(s), \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Você tem \u0002{total}\u0002 recado(s), \u0002{unread}\u0002 não lido(s), de um máximo de \u0002{max}\u0002.", - "You have \u0002{unread}\u0002 new memo(s). Read them with \u0002/msg MemoServ READ NEW\u0002.": "Você tem \u0002{unread}\u0002 recado(s) novo(s). Leia-os com \u0002/msg MemoServ READ NEW\u0002.", "You have access on no channels.": "Você não tem acesso em nenhum canal.", "You have no games. Use \u0002CHALLENGE \u0002.": "Você não tem jogos. Use \u0002CHALLENGE \u0002.", "You have no memo #\u0002{n}\u0002.": "Você não tem o recado #\u0002{n}\u0002.", @@ -1258,7 +1235,6 @@ "your mailbox summary": "resumo da sua caixa de correio", "your memo preferences": "suas preferências de recado", "{challenger} challenges you to {game} (game #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002": "{challenger} desafia você para {game} (jogo #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002", - "{count} ticket(s). \u0002VIEW\u0002 for detail.": "{count} ticket(s). \u0002VIEW\u0002 para detalhes.", "{name} added for \u0002{mask}\u0002 (temporary).": "{name} adicionado para \u0002{mask}\u0002 (temporário).", "{name} added for \u0002{mask}\u0002.": "{name} adicionado para \u0002{mask}\u0002.", "{name} for \u0002{mask}\u0002 removed.": "{name} para \u0002{mask}\u0002 removido.", @@ -1266,7 +1242,6 @@ "{name} updated for \u0002{mask}\u0002.": "{name} atualizado para \u0002{mask}\u0002.", "{nick} declined your challenge #{id}.": "{nick} recusou seu desafio #{id}.", "{num}. {text} — by {by}": "{num}. {text} — por {by}", - "{n} report(s). \u0002VIEW\u0002 for detail.": "{n} denúncia(s). \u0002VIEW\u0002 para detalhes.", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}) — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] por {setter} ({when}) — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}), expires in {ttl} — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] por {setter} ({when}), expira em {ttl} — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] — {reason}", @@ -1283,5 +1258,55 @@ "{who}: I have no record of \u0002{nick}\u0002 talking in \u0002{chan}\u0002.": "{who}: Não tenho registro de \u0002{nick}\u0002 falando em \u0002{chan}\u0002.", "{word} list for \u0002{name}\u0002:": "lista de {word} para \u0002{name}\u0002:", "Type \u0002HELP \u0002 for detail on one command.": "Digite \u0002HELP \u0002 para detalhes sobre um comando.", - "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "Sem ajuda para \u0002{t}\u0002. Digite \u0002HELP\u0002 para a lista de comandos." + "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "Sem ajuda para \u0002{t}\u0002. Digite \u0002HELP\u0002 para a lista de comandos.", + "Reopped \u0002{opped}\u0002 trusted regular in \u0002{chan}\u0002.": "Reopado \u0002{opped}\u0002 frequentador confiável em \u0002{chan}\u0002.", + "Reopped \u0002{opped}\u0002 trusted regulars in \u0002{chan}\u0002.": "Reopados \u0002{opped}\u0002 frequentadores confiáveis em \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 trigger from \u0002{chan}\u0002.": "Removido \u0002{n}\u0002 gatilho de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 triggers from \u0002{chan}\u0002.": "Removidos \u0002{n}\u0002 gatilhos de \u0002{chan}\u0002.", + " Serving {count} channel: {list}": " Atendendo {count} canal: {list}", + " Serving {count} channels: {list}": " Atendendo {count} canais: {list}", + "Removed all \u0002{n}\u0002 bot.": "Removido todo o \u0002{n}\u0002 bot.", + "Removed all \u0002{n}\u0002 bots.": "Removidos todos os \u0002{n}\u0002 bots.", + "Cleared \u0002{n}\u0002 badword pattern from \u0002{chan}\u0002.": "Removido \u0002{n}\u0002 padrão de palavrão de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 badword patterns from \u0002{chan}\u0002.": "Removidos \u0002{n}\u0002 padrões de palavrão de \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kick in \u0002{chan}\u0002.": "O bot vai banir um usuário após \u0002{n}\u0002 expulsão em \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kicks in \u0002{chan}\u0002.": "O bot vai banir um usuário após \u0002{n}\u0002 expulsões em \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entry from \u0002{chan}\u0002's auto-kick list.": "Removida \u0002{count}\u0002 entrada da lista de auto-expulsão de \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entries from \u0002{chan}\u0002's auto-kick list.": "Removidas \u0002{count}\u0002 entradas da lista de auto-expulsão de \u0002{chan}\u0002.", + "Top {shown} of {total} scored identity for \u0002{chan}\u0002.": "As {shown} melhores de {total} identidade pontuada para \u0002{chan}\u0002.", + "Top {shown} of {total} scored identities for \u0002{chan}\u0002.": "As {shown} melhores de {total} identidades pontuadas para \u0002{chan}\u0002.", + "\u0002{n}\u0002 vote will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 voto agora será necessário para um \u0002!votekick\u0002/\u0002!voteban\u0002 em \u0002{chan}\u0002.", + "\u0002{n}\u0002 votes will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 votos agora serão necessários para um \u0002!votekick\u0002/\u0002!voteban\u0002 em \u0002{chan}\u0002.", + "End of flags ({count} entry).": "Fim das permissões ({count} entrada).", + "End of flags ({count} entries).": "Fim das permissões ({count} entradas).", + "Deleted all \u0002{count}\u0002 memo.": "Apagado todo \u0002{count}\u0002 memorando.", + "Deleted all \u0002{count}\u0002 memos.": "Apagados todos os \u0002{count}\u0002 memorandos.", + "Memo sent to \u0002{sent}\u0002 account.": "Memorando enviado para \u0002{sent}\u0002 conta.", + "Memo sent to \u0002{sent}\u0002 accounts.": "Memorando enviado para \u0002{sent}\u0002 contas.", + "End of session list ({count} IP).": "Fim da lista de sessões ({count} IP).", + "End of session list ({count} IPs).": "Fim da lista de sessões ({count} IPs).", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live session.": "\u0002{ip}\u0002 tem \u0002{count}\u0002 sessão ativa.", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live sessions.": "\u0002{ip}\u0002 tem \u0002{count}\u0002 sessões ativas.", + "You have \u0002{total}\u0002 memo, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Você tem \u0002{total}\u0002 memorando, \u0002{unread}\u0002 não lido, de um máximo de \u0002{max}\u0002.", + "You have \u0002{total}\u0002 memos, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Você tem \u0002{total}\u0002 memorandos, \u0002{unread}\u0002 não lidos, de um máximo de \u0002{max}\u0002.", + "End of list — {count} match{more}.": "Fim da lista — {count} resultado{more}.", + "End of list — {count} matches{more}.": "Fim da lista — {count} resultados{more}.", + "Memo sent to \u0002{sent}\u0002 operator.": "Memorando enviado para \u0002{sent}\u0002 operador.", + "Memo sent to \u0002{sent}\u0002 operators.": "Memorando enviado para \u0002{sent}\u0002 operadores.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host AKILL'd.": "CHANKILL em \u0002{chan}\u0002: \u0002{banned}\u0002 host recebeu AKILL.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 hosts AKILL'd.": "CHANKILL em \u0002{chan}\u0002: \u0002{banned}\u0002 hosts receberam AKILL.", + " Auto-join : {count} channel — see \u0002AJOIN LIST\u0002": " Auto-entrada : {count} canal — veja \u0002AJOIN LIST\u0002", + " Auto-join : {count} channels — see \u0002AJOIN LIST\u0002": " Auto-entrada : {count} canais — veja \u0002AJOIN LIST\u0002", + "{count} ticket. \u0002VIEW\u0002 for detail.": "{count} ticket. \u0002VIEW\u0002 para detalhes.", + "{count} tickets. \u0002VIEW\u0002 for detail.": "{count} tickets. \u0002VIEW\u0002 para detalhes.", + "You have \u0002{unread}\u0002 new memo. Read it with \u0002/msg MemoServ READ NEW\u0002.": "Você tem \u0002{unread}\u0002 novo memorando. Leia com \u0002/msg MemoServ READ NEW\u0002.", + "You have \u0002{unread}\u0002 new memos. Read them with \u0002/msg MemoServ READ NEW\u0002.": "Você tem \u0002{unread}\u0002 novos memorandos. Leia com \u0002/msg MemoServ READ NEW\u0002.", + "Cleared \u0002{n}\u0002 notify watch.": "Removida \u0002{n}\u0002 observação de notificação.", + "Cleared \u0002{n}\u0002 notify watches.": "Removidas \u0002{n}\u0002 observações de notificação.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 line seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 linha vista.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 lines seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 linhas vistas.", + "{n} report. \u0002VIEW\u0002 for detail.": "{n} denúncia. \u0002VIEW\u0002 para detalhes.", + "{n} reports. \u0002VIEW\u0002 for detail.": "{n} denúncias. \u0002VIEW\u0002 para detalhes.", + "End of list ({count} group).": "Fim da lista ({count} grupo).", + "End of list ({count} groups).": "Fim da lista ({count} grupos)." } \ No newline at end of file diff --git a/lang/pt.json b/lang/pt.json index 094e6c7..bab27b4 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -61,7 +61,6 @@ "\u0002{chan}\u0002 isn't suspended.": "\u0002{chan}\u0002 não está suspenso.", "\u0002{chan}\u0002 was already set that way.": "\u0002{chan}\u0002 já estava definido dessa forma.", "\u0002{chan}\u0002 will no longer expire.": "\u0002{chan}\u0002 já não vai expirar.", - "\u0002{chan}\u0002 — \u0002{lines}\u0002 line(s) seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 linha(s) vista(s).", "\u0002{code}\u0002 isn't an available language. Available: {list}.": "\u0002{code}\u0002 não é um idioma disponível. Disponíveis: {list}.", "\u0002{d}\u0002 isn't a valid expiry — try 30d, 12h, 45m, or 0.": "\u0002{d}\u0002 não é uma expiração válida — tente 30d, 12h, 45m ou 0.", "\u0002{host}\u0002 is already in use. Please choose another.": "\u0002{host}\u0002 já está em uso. Por favor escolha outro.", @@ -69,7 +68,6 @@ "\u0002{host}\u0002 isn't a valid host (letters, digits, hyphens and dots).": "\u0002{host}\u0002 não é um host válido (letras, dígitos, hífens e pontos).", "\u0002{host}\u0002 isn't a valid host.": "\u0002{host}\u0002 não é um host válido.", "\u0002{host}\u0002 isn't allowed here. Please choose another.": "\u0002{host}\u0002 não é permitido aqui. Por favor escolha outro.", - "\u0002{ip}\u0002 has \u0002{count}\u0002 live session(s).": "\u0002{ip}\u0002 tem \u0002{count}\u0002 sessão(ões) ativa(s).", "\u0002{label}\u0002 access on \u0002{chan}\u0002: \u0002{level}\u0002": "Acesso \u0002{label}\u0002 em \u0002{chan}\u0002: \u0002{level}\u0002", "\u0002{mask}\u0002 isn't on \u0002{chan}\u0002's auto-kick list.": "\u0002{mask}\u0002 não está na lista de auto-kick de \u0002{chan}\u0002.", "\u0002{mask}\u0002 isn't on the ignore list.": "\u0002{mask}\u0002 não está na lista de ignorados.", @@ -88,7 +86,6 @@ "\u0002{nick}\u0002 isn't grouped to your account.": "\u0002{nick}\u0002 não está agrupado à sua conta.", "\u0002{nick}\u0002 isn't here.": "\u0002{nick}\u0002 não está aqui.", "\u0002{nick}\u0002 was last seen {when} ({what}).": "\u0002{nick}\u0002 foi visto pela última vez {when} ({what}).", - "\u0002{n}\u0002 vote(s) will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 voto(s) irão agora accionar um \u0002!votekick\u0002/\u0002!voteban\u0002 em \u0002{chan}\u0002.", "\u0002{pattern}\u0002 isn't a valid regular expression.": "\u0002{pattern}\u0002 não é uma expressão regular válida.", "\u0002{raw}\u0002 isn't a valid \u0002{target}\u0002 mask.": "\u0002{raw}\u0002 não é uma máscara \u0002{target}\u0002 válida.", "\u0002{target}\u0002 had no access to \u0002{chan}\u0002.": "\u0002{target}\u0002 não tinha acesso a \u0002{chan}\u0002.", @@ -143,7 +140,6 @@ " (custom)": " (personalizado)", " (no activity counters yet)": " (ainda sem contadores de atividade)", " About : \u0002{target}\u0002": " Sobre : \u0002{target}\u0002", - " Auto-join : {count} channel(s) — see \u0002AJOIN LIST\u0002": " Auto-join : {count} canal(is) — ver \u0002AJOIN LIST\u0002", " By : \u0002{by}\u0002": " Por : \u0002{by}\u0002", " Description: {desc}": " Descrição : {desc}", " Email : (none set)": " Email : (nenhum definido)", @@ -165,7 +161,6 @@ " Real name: {gecos}": " Nome real: {gecos}", " Reason : {reason}": " Motivo : {reason}", " Registered : {when}": " Registado : {when}", - " Serving {count} channel(s): {list}": " A servir {count} canal(is): {list}", " Staff note : {note}": " Nota da equipa : {note}", " Suspended : by \u0002{by}\u0002 — {reason}": " Suspenso : por \u0002{by}\u0002 — {reason}", " URL : {url}": " URL : {url}", @@ -278,7 +273,6 @@ "Both channels must be registered.": "Ambos os canais têm de estar registados.", "Bots ({count}):": "Bots ({count}):", "CALC and show each die": "CALC e mostrar cada dado", - "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) AKILL'd.": "CHANKILL em \u0002{chan}\u0002: \u0002{banned}\u0002 host(s) com AKILL aplicado.", "Can't activate: \u0002{host}\u0002 is on the forbidden list.": "Não é possível ativar: \u0002{host}\u0002 está na lista de proibidos.", "Can't activate: {msg}": "Não é possível ativar: {msg}", "Challenge \u0002#{id}\u0002 ({game}) sent to \u0002{target}\u0002.": "Desafio \u0002#{id}\u0002 ({game}) enviado a \u0002{target}\u0002.", @@ -291,10 +285,6 @@ "Channels released: {list}.": "Canais libertados: {list}.", "Channels you have access on ({count}):": "Canais a que tem acesso ({count}):", "Classement \u0002{game}\u0002 vide. Sois le premier à gagner !": "Classificação de \u0002{game}\u0002 vazia. Seja o primeiro a ganhar!", - "Cleared \u0002{count}\u0002 entr{suffix} from \u0002{chan}\u0002's auto-kick list.": "Removida(s) \u0002{count}\u0002 entrada{suffix} da lista de auto-kick de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 badword pattern(s) from \u0002{chan}\u0002.": "Removido(s) \u0002{n}\u0002 padrão(ões) de palavras proibidas de \u0002{chan}\u0002.", - "Cleared \u0002{n}\u0002 notify watch(es).": "Removida(s) \u0002{n}\u0002 vigilância(s) de notificação.", - "Cleared \u0002{n}\u0002 trigger(s) from \u0002{chan}\u0002.": "Removido(s) \u0002{n}\u0002 trigger(s) de \u0002{chan}\u0002.", "Cleared \u0002{target}\u0002's access to \u0002{chan}\u0002.": "Removido o acesso de \u0002{target}\u0002 a \u0002{chan}\u0002.", "Cleared the *!*@{host} ban on \u0002{chan}\u0002.": "Removido o ban *!*@{host} em \u0002{chan}\u0002.", "Contact email for \u0002{chan}\u0002 cleared.": "Email de contacto para \u0002{chan}\u0002 removido.", @@ -306,22 +296,17 @@ "Couldn't update \u0002{chan}\u0002 — please try again in a moment.": "Não foi possível atualizar \u0002{chan}\u0002 — por favor tente novamente daqui a instantes.", "DEFCON set to \u0002{level}\u0002 — {desc}.": "DEFCON definido para \u0002{level}\u0002 — {desc}.", "Declined.": "Recusado.", - "Deleted all \u0002{count}\u0002 memo(s).": "Eliminados todos os \u0002{count}\u0002 memo(s).", "Description for \u0002{chan}\u0002 updated.": "Descrição de \u0002{chan}\u0002 atualizada.", "DiceServ rolls dice and evaluates maths for tabletop games. Dice: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Also + - * / ^ %, parentheses, functions (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, and \u00023~2d6\u0002 to repeat.": "O DiceServ lança dados e avalia expressões matemáticas para jogos de tabuleiro. Dados: \u00022d6\u0002, \u0002d20\u0002, \u0002d%\u0002. Também + - * / ^ %, parênteses, funções (sqrt, floor, min, ...), \u0002pi\u0002/\u0002e\u0002, e \u00023~2d6\u0002 para repetir.", "DictServ looks words up in the dict.org dictionaries. Try \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (thesaurus), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, and more — \u0002LOOKUP\u0002 lists them all.": "O DictServ procura palavras nos dicionários do dict.org. Tente \u0002dict\u0002 (WordNet), \u0002define\u0002 (GCIDE), \u0002thes\u0002 (dicionário de sinónimos), \u0002acronym\u0002, \u0002element\u0002, \u0002law\u0002, \u0002bible\u0002, e mais — \u0002LOOKUP\u0002 lista todos.", "Email for \u0002{account}\u0002 cleared.": "Email de \u0002{account}\u0002 removido.", "Email for \u0002{account}\u0002 updated.": "Email de \u0002{account}\u0002 atualizado.", "End of exception list ({count} shown).": "Fim da lista de exceções ({count} mostradas).", - "End of flags ({count} entr{suffix}).": "Fim das flags ({count} entrada{suffix}).", "End of ignore list ({count} shown).": "Fim da lista de ignorados ({count} mostrados).", "End of jupe list ({count} shown).": "Fim da lista de jupes ({count} mostrados).", - "End of list ({count} group(s)).": "Fim da lista ({count} grupo(s)).", - "End of list — {count} match(es){more}.": "Fim da lista — {count} correspondência(s){more}.", "End of notify list ({shown} shown).": "Fim da lista de notificações ({shown} mostradas).", "End of results ({count} shown, newest first — refine the search to narrow).": "Fim dos resultados ({count} mostrados, mais recentes primeiro — refine a pesquisa para restringir).", "End of runtime operator list ({count} shown).": "Fim da lista de operadores de runtime ({count} mostrados).", - "End of session list ({count} IP(s)).": "Fim da lista de sessões ({count} IP(s)).", "End of spam-filter list ({shown} shown).": "Fim da lista de spam-filter ({shown} mostrados).", "End of {name} list ({shown} shown).": "Fim da lista {name} ({shown} mostrados).", "End of {which} bulletins ({count} shown).": "Fim dos boletins {which} ({count} mostrados).", @@ -387,8 +372,6 @@ "Mask must be a \u0002#channel\u0002, a \u0002nick!user@host\u0002 pattern, or a bare nick glob — and no spaces.": "A máscara tem de ser um \u0002#channel\u0002, um padrão \u0002nick!user@host\u0002, ou um glob de alcunha simples — e sem espaços.", "Memo #\u0002{num}\u0002 from \u0002{from}\u0002 ({when}):": "Memo #\u0002{num}\u0002 de \u0002{from}\u0002 ({when}):", "Memo #\u0002{n}\u0002 deleted.": "Memo #\u0002{n}\u0002 eliminado.", - "Memo sent to \u0002{sent}\u0002 account(s).": "Memo enviado a \u0002{sent}\u0002 conta(s).", - "Memo sent to \u0002{sent}\u0002 operator(s).": "Memo enviado a \u0002{sent}\u0002 operador(es).", "Memo sent to \u0002{target}\u0002.": "Memo enviado a \u0002{target}\u0002.", "MemoServ delivers messages to registered users, online or not. You must be identified to use it.": "O MemoServ entrega mensagens a utilizadores registados, online ou não. Tem de estar identificado para o usar.", "Mode lock for \u0002{chan}\u0002 updated.": "Bloqueio de modos de \u0002{chan}\u0002 atualizado.", @@ -485,12 +468,10 @@ "Removed \u0002{mask}\u0002 from \u0002{chan}\u0002's auto-kick list.": "\u0002{mask}\u0002 removido da lista de auto-expulsão de \u0002{chan}\u0002.", "Removed \u0002{target}\u0002 from \u0002{name}\u0002.": "\u0002{target}\u0002 removido de \u0002{name}\u0002.", "Removed \u0002{which}\u0002 bulletin \u0002{n}\u0002.": "Boletim \u0002{which}\u0002 \u0002{n}\u0002 removido.", - "Removed all \u0002{n}\u0002 bot(s).": "Removidos todos os \u0002{n}\u0002 bot(s).", "Removed badword pattern from \u0002{chan}\u0002.": "Padrão de palavrão removido de \u0002{chan}\u0002.", "Removed fingerprint \u0002{fp}\u0002.": "Impressão digital \u0002{fp}\u0002 removida.", "Removed forbidden pattern: {pattern}": "Padrão proibido removido: {pattern}", "Removed the forbid on {label} \u0002{mask}\u0002.": "Proibição de {label} \u0002{mask}\u0002 removida.", - "Reopped \u0002{opped}\u0002 trusted regular(s) in \u0002{chan}\u0002.": "Dei op novamente a \u0002{opped}\u0002 frequentador(es) de confiança em \u0002{chan}\u0002.", "Report \u0002#{id}\u0002 ({state}):": "Denúncia \u0002#{id}\u0002 ({state}):", "Report \u0002#{n}\u0002 closed.": "Denúncia \u0002#{n}\u0002 fechada.", "Report \u0002#{n}\u0002 deleted.": "Denúncia \u0002#{n}\u0002 eliminada.", @@ -881,7 +862,6 @@ "The bot has left \u0002{chan}\u0002.": "O bot saiu de \u0002{chan}\u0002.", "The bot may again kick channel operators in \u0002{chan}\u0002.": "O bot pode voltar a expulsar operadores de canal em \u0002{chan}\u0002.", "The bot may again kick voiced users in \u0002{chan}\u0002.": "O bot pode voltar a expulsar utilizadores com voz em \u0002{chan}\u0002.", - "The bot will ban a user after \u0002{n}\u0002 kick(s) in \u0002{chan}\u0002.": "O bot irá banir um utilizador após \u0002{n}\u0002 expulsão(ões) em \u0002{chan}\u0002.", "The bot will kick without warning in \u0002{chan}\u0002.": "O bot irá expulsar sem aviso em \u0002{chan}\u0002.", "The bot will no longer kick channel operators in \u0002{chan}\u0002.": "O bot deixará de expulsar operadores de canal em \u0002{chan}\u0002.", "The bot will no longer kick voiced users in \u0002{chan}\u0002.": "O bot deixará de expulsar utilizadores com voz em \u0002{chan}\u0002.", @@ -927,7 +907,6 @@ "Ticket \u0002#{id}\u0002 isn't open (or doesn't exist).": "O bilhete \u0002#{id}\u0002 não está aberto (ou não existe).", "Too many failed attempts. Please wait {secs}s and try again.": "Demasiadas tentativas falhadas. Aguarde {secs}s e tente novamente.", "Top talkers:": "Quem mais fala:", - "Top {shown} of {total} scored identit{suffix} for \u0002{chan}\u0002.": "Top {shown} de {total} identidad{suffix} pontuada(s) para \u0002{chan}\u0002.", "Topic for \u0002{chan}\u0002 updated.": "Tópico de \u0002{chan}\u0002 atualizado.", "Trigger #\u0002{n}\u0002 removed from \u0002{chan}\u0002.": "Acionador #\u0002{n}\u0002 removido de \u0002{chan}\u0002.", "Triggers for \u0002{chan}\u0002 ({count}):": "Acionadores de \u0002{chan}\u0002 ({count}):", @@ -958,8 +937,6 @@ "You can't ungroup your main account name.": "Não pode desagrupar o nome da sua conta principal.", "You cannot challenge yourself.": "Não pode desafiar-se a si próprio.", "You don't have an account to confirm.": "Não tem nenhuma conta para confirmar.", - "You have \u0002{total}\u0002 memo(s), \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Tem \u0002{total}\u0002 memo(s), \u0002{unread}\u0002 por ler, de um máximo de \u0002{max}\u0002.", - "You have \u0002{unread}\u0002 new memo(s). Read them with \u0002/msg MemoServ READ NEW\u0002.": "Tem \u0002{unread}\u0002 memo(s) novo(s). Leia-os com \u0002/msg MemoServ READ NEW\u0002.", "You have access on no channels.": "Não tem acesso a nenhum canal.", "You have no games. Use \u0002CHALLENGE \u0002.": "Não tem jogos. Use \u0002CHALLENGE \u0002.", "You have no memo #\u0002{n}\u0002.": "Não tem nenhum memo #\u0002{n}\u0002.", @@ -1258,7 +1235,6 @@ "your mailbox summary": "o resumo da sua caixa de correio", "your memo preferences": "as suas preferências de memos", "{challenger} challenges you to {game} (game #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002": "{challenger} desafia-o para {game} (jogo #{id}). \u0002/msg GamesServ ACCEPT {id}\u0002", - "{count} ticket(s). \u0002VIEW\u0002 for detail.": "{count} bilhete(s). \u0002VIEW\u0002 para detalhes.", "{name} added for \u0002{mask}\u0002 (temporary).": "{name} adicionado para \u0002{mask}\u0002 (temporário).", "{name} added for \u0002{mask}\u0002.": "{name} adicionado para \u0002{mask}\u0002.", "{name} for \u0002{mask}\u0002 removed.": "{name} para \u0002{mask}\u0002 removido.", @@ -1266,7 +1242,6 @@ "{name} updated for \u0002{mask}\u0002.": "{name} atualizado para \u0002{mask}\u0002.", "{nick} declined your challenge #{id}.": "{nick} recusou o seu desafio #{id}.", "{num}. {text} — by {by}": "{num}. {text} — por {by}", - "{n} report(s). \u0002VIEW\u0002 for detail.": "{n} relatório(s). \u0002VIEW\u0002 para detalhes.", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}) — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] por {setter} ({when}) — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] by {setter} ({when}), expires in {ttl} — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] por {setter} ({when}), expira em {ttl} — {reason}", "{n}. \u0002{mask}\u0002 [{flags}] — {reason}": "{n}. \u0002{mask}\u0002 [{flags}] — {reason}", @@ -1283,5 +1258,55 @@ "{who}: I have no record of \u0002{nick}\u0002 talking in \u0002{chan}\u0002.": "{who}: não tenho registo de \u0002{nick}\u0002 a falar em \u0002{chan}\u0002.", "{word} list for \u0002{name}\u0002:": "lista de {word} para \u0002{name}\u0002:", "Type \u0002HELP \u0002 for detail on one command.": "Escreva \u0002HELP \u0002 para detalhes sobre um comando.", - "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "Sem ajuda para \u0002{t}\u0002. Escreva \u0002HELP\u0002 para a lista de comandos." + "No help for \u0002{t}\u0002. Type \u0002HELP\u0002 for the command list.": "Sem ajuda para \u0002{t}\u0002. Escreva \u0002HELP\u0002 para a lista de comandos.", + "Reopped \u0002{opped}\u0002 trusted regular in \u0002{chan}\u0002.": "Op devolvido a \u0002{opped}\u0002 habitual de confiança em \u0002{chan}\u0002.", + "Reopped \u0002{opped}\u0002 trusted regulars in \u0002{chan}\u0002.": "Op devolvido a \u0002{opped}\u0002 habituais de confiança em \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 trigger from \u0002{chan}\u0002.": "Removido \u0002{n}\u0002 gatilho de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 triggers from \u0002{chan}\u0002.": "Removidos \u0002{n}\u0002 gatilhos de \u0002{chan}\u0002.", + " Serving {count} channel: {list}": " A servir {count} canal: {list}", + " Serving {count} channels: {list}": " A servir {count} canais: {list}", + "Removed all \u0002{n}\u0002 bot.": "Removido \u0002{n}\u0002 bot.", + "Removed all \u0002{n}\u0002 bots.": "Removidos todos os \u0002{n}\u0002 bots.", + "Cleared \u0002{n}\u0002 badword pattern from \u0002{chan}\u0002.": "Removido \u0002{n}\u0002 padrão de palavrão de \u0002{chan}\u0002.", + "Cleared \u0002{n}\u0002 badword patterns from \u0002{chan}\u0002.": "Removidos \u0002{n}\u0002 padrões de palavrão de \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kick in \u0002{chan}\u0002.": "O bot irá banir um utilizador após \u0002{n}\u0002 expulsão em \u0002{chan}\u0002.", + "The bot will ban a user after \u0002{n}\u0002 kicks in \u0002{chan}\u0002.": "O bot irá banir um utilizador após \u0002{n}\u0002 expulsões em \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entry from \u0002{chan}\u0002's auto-kick list.": "Removida \u0002{count}\u0002 entrada da lista de auto-kick de \u0002{chan}\u0002.", + "Cleared \u0002{count}\u0002 entries from \u0002{chan}\u0002's auto-kick list.": "Removidas \u0002{count}\u0002 entradas da lista de auto-kick de \u0002{chan}\u0002.", + "Top {shown} of {total} scored identity for \u0002{chan}\u0002.": "Top {shown} de {total} identidade pontuada para \u0002{chan}\u0002.", + "Top {shown} of {total} scored identities for \u0002{chan}\u0002.": "Top {shown} de {total} identidades pontuadas para \u0002{chan}\u0002.", + "\u0002{n}\u0002 vote will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 voto passará a bastar para um \u0002!votekick\u0002/\u0002!voteban\u0002 em \u0002{chan}\u0002.", + "\u0002{n}\u0002 votes will now carry a \u0002!votekick\u0002/\u0002!voteban\u0002 in \u0002{chan}\u0002.": "\u0002{n}\u0002 votos passarão a bastar para um \u0002!votekick\u0002/\u0002!voteban\u0002 em \u0002{chan}\u0002.", + "End of flags ({count} entry).": "Fim das flags ({count} entrada).", + "End of flags ({count} entries).": "Fim das flags ({count} entradas).", + "Deleted all \u0002{count}\u0002 memo.": "Eliminado \u0002{count}\u0002 memo.", + "Deleted all \u0002{count}\u0002 memos.": "Eliminados todos os \u0002{count}\u0002 memos.", + "Memo sent to \u0002{sent}\u0002 account.": "Memo enviado para \u0002{sent}\u0002 conta.", + "Memo sent to \u0002{sent}\u0002 accounts.": "Memo enviado para \u0002{sent}\u0002 contas.", + "End of session list ({count} IP).": "Fim da lista de sessões ({count} IP).", + "End of session list ({count} IPs).": "Fim da lista de sessões ({count} IPs).", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live session.": "\u0002{ip}\u0002 tem \u0002{count}\u0002 sessão ativa.", + "\u0002{ip}\u0002 has \u0002{count}\u0002 live sessions.": "\u0002{ip}\u0002 tem \u0002{count}\u0002 sessões ativas.", + "You have \u0002{total}\u0002 memo, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Tem \u0002{total}\u0002 memo, \u0002{unread}\u0002 por ler, de um máximo de \u0002{max}\u0002.", + "You have \u0002{total}\u0002 memos, \u0002{unread}\u0002 unread, of a maximum \u0002{max}\u0002.": "Tem \u0002{total}\u0002 memos, \u0002{unread}\u0002 por ler, de um máximo de \u0002{max}\u0002.", + "End of list — {count} match{more}.": "Fim da lista — {count} correspondência{more}.", + "End of list — {count} matches{more}.": "Fim da lista — {count} correspondências{more}.", + "Memo sent to \u0002{sent}\u0002 operator.": "Memo enviado para \u0002{sent}\u0002 operador.", + "Memo sent to \u0002{sent}\u0002 operators.": "Memo enviado para \u0002{sent}\u0002 operadores.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 host AKILL'd.": "CHANKILL em \u0002{chan}\u0002: \u0002{banned}\u0002 host recebeu AKILL.", + "CHANKILL on \u0002{chan}\u0002: \u0002{banned}\u0002 hosts AKILL'd.": "CHANKILL em \u0002{chan}\u0002: \u0002{banned}\u0002 hosts receberam AKILL.", + " Auto-join : {count} channel — see \u0002AJOIN LIST\u0002": " Auto-join : {count} canal — ver \u0002AJOIN LIST\u0002", + " Auto-join : {count} channels — see \u0002AJOIN LIST\u0002": " Auto-join : {count} canais — ver \u0002AJOIN LIST\u0002", + "{count} ticket. \u0002VIEW\u0002 for detail.": "{count} ticket. \u0002VIEW\u0002 para detalhes.", + "{count} tickets. \u0002VIEW\u0002 for detail.": "{count} tickets. \u0002VIEW\u0002 para detalhes.", + "You have \u0002{unread}\u0002 new memo. Read it with \u0002/msg MemoServ READ NEW\u0002.": "Tem \u0002{unread}\u0002 memo novo. Leia-o com \u0002/msg MemoServ READ NEW\u0002.", + "You have \u0002{unread}\u0002 new memos. Read them with \u0002/msg MemoServ READ NEW\u0002.": "Tem \u0002{unread}\u0002 memos novos. Leia-os com \u0002/msg MemoServ READ NEW\u0002.", + "Cleared \u0002{n}\u0002 notify watch.": "Removido \u0002{n}\u0002 alerta de notificação.", + "Cleared \u0002{n}\u0002 notify watches.": "Removidos \u0002{n}\u0002 alertas de notificação.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 line seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 linha vista.", + "\u0002{chan}\u0002 — \u0002{lines}\u0002 lines seen.": "\u0002{chan}\u0002 — \u0002{lines}\u0002 linhas vistas.", + "{n} report. \u0002VIEW\u0002 for detail.": "{n} denúncia. \u0002VIEW\u0002 para detalhes.", + "{n} reports. \u0002VIEW\u0002 for detail.": "{n} denúncias. \u0002VIEW\u0002 para detalhes.", + "End of list ({count} group).": "Fim da lista ({count} grupo).", + "End of list ({count} groups).": "Fim da lista ({count} grupos)." } \ No newline at end of file diff --git a/modules/botserv/src/badwords.rs b/modules/botserv/src/badwords.rs index a634cc0..f9386fe 100644 --- a/modules/botserv/src/badwords.rs +++ b/modules/botserv/src/badwords.rs @@ -40,7 +40,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: } } Some("CLEAR") => match db.badword_clear(chan) { - Ok(n) => ctx.notice(me, from.uid, t!(ctx, "Cleared \x02{n}\x02 badword pattern(s) from \x02{chan}\x02.", n = n, chan = chan)), + Ok(n) => ctx.notice(me, from.uid, echo_api::plural!(ctx, n, one = "Cleared \x02{n}\x02 badword pattern from \x02{chan}\x02.", other = "Cleared \x02{n}\x02 badword patterns from \x02{chan}\x02.", n = n, chan = chan)), Err(_) => reg_error(me, from, chan, ctx), }, None | Some("LIST") => { diff --git a/modules/botserv/src/bot.rs b/modules/botserv/src/bot.rs index 5fa0043..a118722 100644 --- a/modules/botserv/src/bot.rs +++ b/modules/botserv/src/bot.rs @@ -45,7 +45,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: }; if nick == "*" { match db.bot_del_all() { - Ok(n) => ctx.notice(me, from.uid, t!(ctx, "Removed all \x02{n}\x02 bot(s).", n = n)), + Ok(n) => ctx.notice(me, from.uid, echo_api::plural!(ctx, n, one = "Removed all \x02{n}\x02 bot.", other = "Removed all \x02{n}\x02 bots.", n = n)), Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."), } return; diff --git a/modules/botserv/src/info.rs b/modules/botserv/src/info.rs index f892b03..9da981a 100644 --- a/modules/botserv/src/info.rs +++ b/modules/botserv/src/info.rs @@ -50,6 +50,6 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: if channels.is_empty() { ctx.notice(me, from.uid, " Not assigned to any channel."); } else { - ctx.notice(me, from.uid, t!(ctx, " Serving {count} channel(s): {list}", count = channels.len(), list = channels.join(", "))); + ctx.notice(me, from.uid, echo_api::plural!(ctx, channels.len(), one = " Serving {count} channel: {list}", other = " Serving {count} channels: {list}", count = channels.len(), list = channels.join(", "))); } } diff --git a/modules/botserv/src/kick.rs b/modules/botserv/src/kick.rs index 70afedb..306a340 100644 --- a/modules/botserv/src/kick.rs +++ b/modules/botserv/src/kick.rs @@ -17,7 +17,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: match args.get(3).and_then(|s| s.parse::().ok()) { Some(n) => match db.set_ttb(chan, n) { Ok(()) if n == 0 => ctx.notice(me, from.uid, t!(ctx, "The bot will only kick (not ban) in \x02{chan}\x02.", chan = chan)), - Ok(()) => ctx.notice(me, from.uid, t!(ctx, "The bot will ban a user after \x02{n}\x02 kick(s) in \x02{chan}\x02.", n = n, chan = chan)), + Ok(()) => ctx.notice(me, from.uid, echo_api::plural!(ctx, n, one = "The bot will ban a user after \x02{n}\x02 kick in \x02{chan}\x02.", other = "The bot will ban a user after \x02{n}\x02 kicks in \x02{chan}\x02.", n = n, chan = chan)), Err(_) => reg_error(me, from, chan, ctx), }, None => ctx.notice(me, from.uid, "Syntax: KICK <#channel> TTB "), diff --git a/modules/botserv/src/set.rs b/modules/botserv/src/set.rs index 1a51b65..d465d3c 100644 --- a/modules/botserv/src/set.rs +++ b/modules/botserv/src/set.rs @@ -47,7 +47,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: match args.get(3).and_then(|s| s.parse::().ok()) { Some(n) => match db.set_votekick(chan, n) { Ok(()) if n == 0 => ctx.notice(me, from.uid, t!(ctx, "\x02!votekick\x02 is now disabled in \x02{chan}\x02.", chan = chan)), - Ok(()) => ctx.notice(me, from.uid, t!(ctx, "\x02{n}\x02 vote(s) will now carry a \x02!votekick\x02/\x02!voteban\x02 in \x02{chan}\x02.", n = n, chan = chan)), + Ok(()) => ctx.notice(me, from.uid, echo_api::plural!(ctx, n, one = "\x02{n}\x02 vote will now carry a \x02!votekick\x02/\x02!voteban\x02 in \x02{chan}\x02.", other = "\x02{n}\x02 votes will now carry a \x02!votekick\x02/\x02!voteban\x02 in \x02{chan}\x02.", n = n, chan = chan)), Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."), }, None => ctx.notice(me, from.uid, "Syntax: SET <#channel> VOTEKICK (0 to disable)"), diff --git a/modules/botserv/src/trigger.rs b/modules/botserv/src/trigger.rs index 5431653..4d5ed49 100644 --- a/modules/botserv/src/trigger.rs +++ b/modules/botserv/src/trigger.rs @@ -53,7 +53,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: } } Some("CLEAR") => match db.trigger_clear(chan) { - Ok(n) => ctx.notice(me, from.uid, t!(ctx, "Cleared \x02{n}\x02 trigger(s) from \x02{chan}\x02.", n = n, chan = chan)), + Ok(n) => ctx.notice(me, from.uid, echo_api::plural!(ctx, n, one = "Cleared \x02{n}\x02 trigger from \x02{chan}\x02.", other = "Cleared \x02{n}\x02 triggers from \x02{chan}\x02.", n = n, chan = chan)), Err(_) => reg_error(me, from, chan, ctx), }, None | Some("LIST") => { diff --git a/modules/chanfix/src/fix.rs b/modules/chanfix/src/fix.rs index 3ccb2c1..e75b22a 100644 --- a/modules/chanfix/src/fix.rs +++ b/modules/chanfix/src/fix.rs @@ -49,6 +49,6 @@ pub fn handle(me: &str, from: &Sender, chan: Option<&str>, ctx: &mut ServiceCtx, if opped == 0 { ctx.notice(me, from.uid, t!(ctx, "None of \x02{chan}\x02's trusted regulars are here right now.", chan = chan)); } else { - ctx.notice(me, from.uid, t!(ctx, "Reopped \x02{opped}\x02 trusted regular(s) in \x02{chan}\x02.", opped = opped, chan = chan)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, opped, one = "Reopped \x02{opped}\x02 trusted regular in \x02{chan}\x02.", other = "Reopped \x02{opped}\x02 trusted regulars in \x02{chan}\x02.", opped = opped, chan = chan)); } } diff --git a/modules/chanfix/src/scores.rs b/modules/chanfix/src/scores.rs index 15dc1a5..88bc886 100644 --- a/modules/chanfix/src/scores.rs +++ b/modules/chanfix/src/scores.rs @@ -14,5 +14,5 @@ pub fn handle(me: &str, from: &Sender, chan: Option<&str>, ctx: &mut ServiceCtx, for (id, score) in scores.iter().take(15) { ctx.notice(me, from.uid, t!(ctx, " \x02{score}\x02 {id}", score = score, id = id)); } - ctx.notice(me, from.uid, t!(ctx, "Top {shown} of {total} scored identit{suffix} for \x02{chan}\x02.", shown = scores.len().min(15), total = scores.len(), suffix = if scores.len() == 1 { "y" } else { "ies" }, chan = chan)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, scores.len(), one = "Top {shown} of {total} scored identity for \x02{chan}\x02.", other = "Top {shown} of {total} scored identities for \x02{chan}\x02.", shown = scores.len().min(15), total = scores.len(), chan = chan)); } diff --git a/modules/chanserv/src/akick.rs b/modules/chanserv/src/akick.rs index a5041fe..251aa84 100644 --- a/modules/chanserv/src/akick.rs +++ b/modules/chanserv/src/akick.rs @@ -111,7 +111,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: ctx.channel_mode(me, chan, &format!("-b {mask}")); } } - ctx.notice(me, from.uid, t!(ctx, "Cleared \x02{count}\x02 entr{suffix} from \x02{chan}\x02's auto-kick list.", count = masks.len(), suffix = if masks.len() == 1 { "y" } else { "ies" }, chan = chan)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, masks.len(), one = "Cleared \x02{count}\x02 entry from \x02{chan}\x02's auto-kick list.", other = "Cleared \x02{count}\x02 entries from \x02{chan}\x02's auto-kick list.", count = masks.len(), chan = chan)); } _ => ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD [reason] | DEL | LIST | CLEAR"), } diff --git a/modules/chanserv/src/flags.rs b/modules/chanserv/src/flags.rs index 49e9d78..322f209 100644 --- a/modules/chanserv/src/flags.rs +++ b/modules/chanserv/src/flags.rs @@ -26,7 +26,7 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv for a in &info.access { ctx.notice(me, from.uid, t!(ctx, " \x02{account}\x02: \x02{flags}\x02", account = a.account, flags = Flags::from_level(&a.level).to_letters())); } - ctx.notice(me, from.uid, t!(ctx, "End of flags ({count} entr{suffix}).", count = info.access.len() + 1, suffix = if info.access.is_empty() { "y" } else { "ies" })); + ctx.notice(me, from.uid, echo_api::plural!(ctx, info.access.len() + 1, one = "End of flags ({count} entry).", other = "End of flags ({count} entries).", count = info.access.len() + 1)); return; }; diff --git a/modules/groupserv/src/list.rs b/modules/groupserv/src/list.rs index 63bbacf..c89cc87 100644 --- a/modules/groupserv/src/list.rs +++ b/modules/groupserv/src/list.rs @@ -16,5 +16,5 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store) for n in &names { ctx.notice(me, from.uid, t!(ctx, " \x02{n}\x02", n = n)); } - ctx.notice(me, from.uid, t!(ctx, "End of list ({count} group(s)).", count = names.len())); + ctx.notice(me, from.uid, echo_api::plural!(ctx, names.len(), one = "End of list ({count} group).", other = "End of list ({count} groups).", count = names.len())); } diff --git a/modules/helpserv/src/list.rs b/modules/helpserv/src/list.rs index 2aca521..9d7746f 100644 --- a/modules/helpserv/src/list.rs +++ b/modules/helpserv/src/list.rs @@ -20,5 +20,5 @@ pub fn handle(me: &str, from: &Sender, arg: Option<&str>, ctx: &mut ServiceCtx, let short: String = t.message.chars().take(60).collect(); ctx.notice(me, from.uid, t!(ctx, "\x02#{id}\x02 {requester} — {short}{state}", id = t.id, requester = t.requester, short = short, state = state)); } - ctx.notice(me, from.uid, t!(ctx, "{count} ticket(s). \x02VIEW\x02 for detail.", count = tickets.len())); + ctx.notice(me, from.uid, echo_api::plural!(ctx, tickets.len(), one = "{count} ticket. \x02VIEW\x02 for detail.", other = "{count} tickets. \x02VIEW\x02 for detail.", count = tickets.len())); } diff --git a/modules/memoserv/src/del.rs b/modules/memoserv/src/del.rs index 9edc931..6cb89bc 100644 --- a/modules/memoserv/src/del.rs +++ b/modules/memoserv/src/del.rs @@ -9,7 +9,7 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S for i in (0..count).rev() { db.memo_del(account, i); } - ctx.notice(me, from.uid, t!(ctx, "Deleted all \x02{count}\x02 memo(s).", count = count)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, count, one = "Deleted all \x02{count}\x02 memo.", other = "Deleted all \x02{count}\x02 memos.", count = count)); } Some(numstr) => { let Some(n) = numstr.parse::().ok().filter(|n| *n >= 1) else { diff --git a/modules/memoserv/src/info.rs b/modules/memoserv/src/info.rs index 2bc3a44..9a0fc35 100644 --- a/modules/memoserv/src/info.rs +++ b/modules/memoserv/src/info.rs @@ -1,4 +1,4 @@ -use echo_api::{t, Sender, ServiceCtx, Store}; +use echo_api::{Sender, ServiceCtx, Store}; // INFO: a summary of your mailbox (total, unread, capacity). pub fn handle(me: &str, from: &Sender, account: &str, ctx: &mut ServiceCtx, db: &mut dyn Store) { @@ -7,6 +7,6 @@ pub fn handle(me: &str, from: &Sender, account: &str, ctx: &mut ServiceCtx, db: ctx.notice( me, from.uid, - t!(ctx, "You have \x02{total}\x02 memo(s), \x02{unread}\x02 unread, of a maximum \x02{max}\x02.", total = total, unread = unread, max = super::MAX_MEMOS), + echo_api::plural!(ctx, total, one = "You have \x02{total}\x02 memo, \x02{unread}\x02 unread, of a maximum \x02{max}\x02.", other = "You have \x02{total}\x02 memos, \x02{unread}\x02 unread, of a maximum \x02{max}\x02.", total = total, unread = unread, max = super::MAX_MEMOS), ); } diff --git a/modules/memoserv/src/sendall.rs b/modules/memoserv/src/sendall.rs index 23453f5..c619bc6 100644 --- a/modules/memoserv/src/sendall.rs +++ b/modules/memoserv/src/sendall.rs @@ -1,4 +1,4 @@ -use echo_api::{t, Priv, Sender, ServiceCtx, Store}; +use echo_api::{Priv, Sender, ServiceCtx, Store}; // SENDALL : leave a memo on every registered account. Admin-only, for // network-wide announcements that persist until read. @@ -20,5 +20,5 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S sent += 1; } } - ctx.notice(me, from.uid, t!(ctx, "Memo sent to \x02{sent}\x02 account(s).", sent = sent)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, sent, one = "Memo sent to \x02{sent}\x02 account.", other = "Memo sent to \x02{sent}\x02 accounts.", sent = sent)); } diff --git a/modules/memoserv/src/staff.rs b/modules/memoserv/src/staff.rs index af7b3b7..0386194 100644 --- a/modules/memoserv/src/staff.rs +++ b/modules/memoserv/src/staff.rs @@ -1,4 +1,4 @@ -use echo_api::{t, NetView, Priv, Sender, ServiceCtx, Store}; +use echo_api::{NetView, Priv, Sender, ServiceCtx, Store}; // STAFF : leave a memo on every operator's account. Admin-only, for // notes to the staff team that persist until read. Recipients are the union of @@ -29,5 +29,5 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S sent += 1; } } - ctx.notice(me, from.uid, t!(ctx, "Memo sent to \x02{sent}\x02 operator(s).", sent = sent)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, sent, one = "Memo sent to \x02{sent}\x02 operator.", other = "Memo sent to \x02{sent}\x02 operators.", sent = sent)); } diff --git a/modules/nickserv/src/info.rs b/modules/nickserv/src/info.rs index af00402..51b6bab 100644 --- a/modules/nickserv/src/info.rs +++ b/modules/nickserv/src/info.rs @@ -39,7 +39,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: } let ajoin = db.ajoin_list(&acct.name); if !ajoin.is_empty() { - ctx.notice(me, from.uid, t!(ctx, " Auto-join : {count} channel(s) — see \x02AJOIN LIST\x02", count = ajoin.len())); + ctx.notice(me, from.uid, echo_api::plural!(ctx, ajoin.len(), one = " Auto-join : {count} channel — see \x02AJOIN LIST\x02", other = " Auto-join : {count} channels — see \x02AJOIN LIST\x02", count = ajoin.len())); } } // A staff note is for operators' eyes only, never the account's owner. diff --git a/modules/nickserv/src/list.rs b/modules/nickserv/src/list.rs index 60b0132..6a0d07e 100644 --- a/modules/nickserv/src/list.rs +++ b/modules/nickserv/src/list.rs @@ -20,5 +20,5 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: ctx.notice(me, from.uid, t!(ctx, " \x02{name}\x02 registered {when}{flag}", name = a.name, when = human_time(a.ts), flag = flag)); } let more = if matches.len() > MAX_SHOWN { t!(ctx, ", showing the first {max}", max = MAX_SHOWN) } else { String::new() }; - ctx.notice(me, from.uid, t!(ctx, "End of list — {count} match(es){more}.", count = matches.len(), more = more)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, matches.len(), one = "End of list — {count} match{more}.", other = "End of list — {count} matches{more}.", count = matches.len(), more = more)); } diff --git a/modules/nickserv/src/update.rs b/modules/nickserv/src/update.rs index 20b6cbb..5e63d98 100644 --- a/modules/nickserv/src/update.rs +++ b/modules/nickserv/src/update.rs @@ -1,5 +1,4 @@ use echo_api::{Sender, ServiceCtx, Store}; -use echo_api::t; // UPDATE: re-apply your account's auto-joins and vhost, and re-check for waiting // memos, without having to re-identify. @@ -16,7 +15,7 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { } let unread = db.unread_memos(account); if unread > 0 { - ctx.notice(me, from.uid, t!(ctx, "You have \x02{unread}\x02 new memo(s). Read them with \x02/msg MemoServ READ NEW\x02.", unread = unread)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, unread, one = "You have \x02{unread}\x02 new memo. Read it with \x02/msg MemoServ READ NEW\x02.", other = "You have \x02{unread}\x02 new memos. Read them with \x02/msg MemoServ READ NEW\x02.", unread = unread)); } ctx.notice(me, from.uid, "Your status has been refreshed."); } diff --git a/modules/operserv/src/chankill.rs b/modules/operserv/src/chankill.rs index 29419b4..6429bac 100644 --- a/modules/operserv/src/chankill.rs +++ b/modules/operserv/src/chankill.rs @@ -34,5 +34,5 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: banned += 1; } } - ctx.notice(me, from.uid, t!(ctx, "CHANKILL on \x02{chan}\x02: \x02{banned}\x02 host(s) AKILL'd.", chan = chan, banned = banned)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, banned, one = "CHANKILL on \x02{chan}\x02: \x02{banned}\x02 host AKILL'd.", other = "CHANKILL on \x02{chan}\x02: \x02{banned}\x02 hosts AKILL'd.", chan = chan, banned = banned)); } diff --git a/modules/operserv/src/notify.rs b/modules/operserv/src/notify.rs index bdaf65f..ccf9b0f 100644 --- a/modules/operserv/src/notify.rs +++ b/modules/operserv/src/notify.rs @@ -20,7 +20,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: Some("VIEW") => list(me, from, true, args.get(2).copied(), ctx, db), Some("CLEAR") => match db.notify_clear() { Ok(0) => ctx.notice(me, from.uid, "The notify list is already empty."), - Ok(n) => ctx.notice(me, from.uid, t!(ctx, "Cleared \x02{n}\x02 notify watch(es).", n = n)), + Ok(n) => ctx.notice(me, from.uid, echo_api::plural!(ctx, n, one = "Cleared \x02{n}\x02 notify watch.", other = "Cleared \x02{n}\x02 notify watches.", n = n)), Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."), }, _ => ctx.notice(me, from.uid, SYNTAX), diff --git a/modules/operserv/src/session.rs b/modules/operserv/src/session.rs index 9479dd5..194451a 100644 --- a/modules/operserv/src/session.rs +++ b/modules/operserv/src/session.rs @@ -19,14 +19,14 @@ pub fn handle_session(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceC for (ip, n) in &over { ctx.notice(me, from.uid, t!(ctx, " \x02{n}\x02 sessions from \x02{ip}\x02", n = n, ip = ip)); } - ctx.notice(me, from.uid, t!(ctx, "End of session list ({count} IP(s)).", count = over.len())); + ctx.notice(me, from.uid, echo_api::plural!(ctx, over.len(), one = "End of session list ({count} IP).", other = "End of session list ({count} IPs).", count = over.len())); } Some("VIEW") => { let Some(&ip) = args.get(2) else { ctx.notice(me, from.uid, "Syntax: SESSION VIEW "); return; }; - ctx.notice(me, from.uid, t!(ctx, "\x02{ip}\x02 has \x02{count}\x02 live session(s).", ip = ip, count = net.session_count(ip))); + ctx.notice(me, from.uid, echo_api::plural!(ctx, net.session_count(ip), one = "\x02{ip}\x02 has \x02{count}\x02 live session.", other = "\x02{ip}\x02 has \x02{count}\x02 live sessions.", ip = ip, count = net.session_count(ip))); } _ => ctx.notice(me, from.uid, "Syntax: SESSION LIST | SESSION VIEW "), } diff --git a/modules/reportserv/src/list.rs b/modules/reportserv/src/list.rs index 05286ae..3544682 100644 --- a/modules/reportserv/src/list.rs +++ b/modules/reportserv/src/list.rs @@ -16,5 +16,5 @@ pub fn handle(me: &str, from: &Sender, arg: Option<&str>, ctx: &mut ServiceCtx, let short: String = r.reason.chars().take(60).collect(); ctx.notice(me, from.uid, t!(ctx, "\x02#{id}\x02 {reporter} → \x02{target}\x02: {short}{flag}", id = r.id, reporter = r.reporter, target = r.target, short = short, flag = flag)); } - ctx.notice(me, from.uid, t!(ctx, "{n} report(s). \x02VIEW\x02 for detail.", n = reports.len())); + ctx.notice(me, from.uid, echo_api::plural!(ctx, reports.len(), one = "{n} report. \x02VIEW\x02 for detail.", other = "{n} reports. \x02VIEW\x02 for detail.", n = reports.len())); } diff --git a/modules/statserv/src/channel.rs b/modules/statserv/src/channel.rs index ebeb290..66f60e9 100644 --- a/modules/statserv/src/channel.rs +++ b/modules/statserv/src/channel.rs @@ -10,7 +10,7 @@ pub fn handle(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, net: &d ctx.notice(me, from.uid, t!(ctx, "No activity recorded for \x02{chan}\x02 yet.", chan = chan)); return; }; - ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 — \x02{lines}\x02 line(s) seen.", chan = chan, lines = lines)); + ctx.notice(me, from.uid, echo_api::plural!(ctx, lines, one = "\x02{chan}\x02 — \x02{lines}\x02 line seen.", other = "\x02{chan}\x02 — \x02{lines}\x02 lines seen.", chan = chan, lines = lines)); if top.is_empty() { return; } diff --git a/src/engine/tests.rs b/src/engine/tests.rs index 8d4eea0..8c6d730 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -3308,7 +3308,7 @@ // Per-channel view. let out = ss(&mut e, "#c"); - assert!(out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("line(s) seen"))), "channel lines: {out:?}"); + assert!(out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("lines seen"))), "channel lines: {out:?}"); assert!(out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("spammer") && text.contains("3"))), "top talker: {out:?}"); // Global view (oper) shows the shared counters. let out = ss(&mut e, "SERVER"); diff --git a/src/i18n_check.rs b/src/i18n_check.rs new file mode 100644 index 0000000..c338e79 --- /dev/null +++ b/src/i18n_check.rs @@ -0,0 +1,175 @@ +//! Translation-catalog guards, run as `cargo test`. These keep the shipped +//! `lang/.json` catalogs complete and consistent with the source, so a +//! stray English edit or a dropped placeholder fails the build instead of +//! silently degrading a language at runtime. + +use regex::Regex; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::PathBuf; + +const ROOT: &str = env!("CARGO_MANIFEST_DIR"); + +// Modules kept intentionally English-only (staff/dev tools), excluded from the +// source-coverage check. +const ENGLISH_ONLY: &[&str] = &["debugserv", "example"]; + +fn load_catalogs() -> HashMap> { + let dir = PathBuf::from(ROOT).join("lang"); + let mut out = HashMap::new(); + for entry in fs::read_dir(&dir).expect("read lang/") { + let path = entry.unwrap().path(); + if path.extension().and_then(|s| s.to_str()) != Some("json") { + continue; + } + let code = path.file_stem().unwrap().to_str().unwrap().to_string(); + let data = fs::read_to_string(&path).unwrap(); + let map: HashMap = + serde_json::from_str(&data).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + out.insert(code, map); + } + assert!(!out.is_empty(), "no catalogs found in lang/"); + out +} + +/// The sorted multiset of `{...}` tokens in a string. +fn placeholders(s: &str) -> Vec<&str> { + let mut v = Vec::new(); + let bytes = s.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'{' { + if let Some(rel) = s[i..].find('}') { + v.push(&s[i..i + rel + 1]); + i += rel + 1; + continue; + } + } + i += 1; + } + v.sort_unstable(); + v +} + +/// Every translation must carry exactly the same `{placeholder}` tokens as its +/// English key — otherwise a substitution silently no-ops or leaks a raw token. +#[test] +fn placeholder_parity() { + let cats = load_catalogs(); + let mut bad = Vec::new(); + for (code, map) in &cats { + for (key, val) in map { + if placeholders(key) != placeholders(val) { + bad.push(format!("[{code}] {key:?}\n -> {val:?}")); + } + } + } + assert!(bad.is_empty(), "placeholder mismatches:\n{}", bad.join("\n")); +} + +/// All catalogs must cover the exact same set of message ids, so no language +/// quietly lags behind another. +#[test] +fn catalogs_share_one_keyset() { + let cats = load_catalogs(); + let reference: HashSet<&String> = cats.values().next().unwrap().keys().collect(); + let mut problems = Vec::new(); + for (code, map) in &cats { + let keys: HashSet<&String> = map.keys().collect(); + let missing = reference.difference(&keys).count(); + let extra = keys.difference(&reference).count(); + if missing != 0 || extra != 0 { + problems.push(format!("[{code}] missing {missing}, extra {extra}")); + } + } + assert!(problems.is_empty(), "catalog keysets differ:\n{}", problems.join("\n")); +} + +/// Turn a Rust string literal's escape sequences into the runtime message id, so +/// scraped templates match the catalog keys byte for byte. +fn unescape(s: &str) -> String { + let mut out = String::new(); + let mut chars = s.char_indices().peekable(); + while let Some((i, c)) = chars.next() { + if c != '\\' { + out.push(c); + continue; + } + match chars.next() { + Some((_, 'x')) => { + let hex = &s[i + 2..i + 4]; + out.push(u8::from_str_radix(hex, 16).unwrap() as char); + chars.next(); // skip the two hex digits + chars.next(); + } + Some((_, 'n')) => out.push('\n'), + Some((_, 't')) => out.push('\t'), + Some((_, 'r')) => out.push('\r'), + Some((_, '0')) => out.push('\0'), + Some((_, other)) => out.push(other), + None => {} + } + } + out +} + +fn module_sources() -> Vec { + let mut files = Vec::new(); + let modules = PathBuf::from(ROOT).join("modules"); + let mut stack = vec![modules]; + while let Some(dir) = stack.pop() { + let Ok(rd) = fs::read_dir(&dir) else { continue }; + for entry in rd.flatten() { + let path = entry.path(); + if path.is_dir() { + let name = path.file_name().and_then(|s| s.to_str()).unwrap_or(""); + if ENGLISH_ONLY.contains(&name) { + continue; + } + stack.push(path); + } else if path.extension().and_then(|s| s.to_str()) == Some("rs") { + files.push(path); + } + } + } + files +} + +/// Every `t!` and `plural!` template used in a translated module must exist in +/// the reference catalog, so a new user-facing string can't ship untranslated. +#[test] +fn every_template_is_translated() { + let cats = load_catalogs(); + // Any catalog works as the reference — `catalogs_share_one_keyset` proves + // they agree; use French, the network default. + let reference = cats.get("fr").expect("fr catalog"); + let str_lit = r#""((?:[^"\\]|\\.)*)""#; + let t_re = Regex::new(&format!(r"\bt!\s*\(\s*ctx\s*,\s*{str_lit}")).unwrap(); + let plural_re = + Regex::new(&format!(r"(?s)plural!\s*\(\s*ctx\s*,.*?one\s*=\s*{str_lit}\s*,\s*other\s*=\s*{str_lit}")).unwrap(); + + let mut missing = Vec::new(); + for path in module_sources() { + let src = fs::read_to_string(&path).unwrap(); + let rel = path.strip_prefix(ROOT).unwrap_or(&path).display().to_string(); + for cap in t_re.captures_iter(&src) { + let id = unescape(&cap[1]); + if !reference.contains_key(&id) { + missing.push(format!("{rel}: t! {id:?}")); + } + } + for cap in plural_re.captures_iter(&src) { + for g in [1usize, 2] { + let id = unescape(&cap[g]); + if !reference.contains_key(&id) { + missing.push(format!("{rel}: plural! {id:?}")); + } + } + } + } + assert!( + missing.is_empty(), + "these templates have no catalog entry:\n{}", + missing.join("\n") + ); +} diff --git a/src/main.rs b/src/main.rs index 7d84b6c..fdf5447 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,8 @@ mod keycard; mod link; mod migrate; mod proto; +#[cfg(test)] +mod i18n_check; use anyhow::Result; use std::sync::Arc;