From c246119809180c5a15a2f052d4f8233434909a84 Mon Sep 17 00:00:00 2001 From: Hengfei Yang Date: Sun, 16 Aug 2026 09:27:40 +0000 Subject: [PATCH] fix(i18n): stop translated placeholder names crashing localized pages (#13833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #13751 ## The crash vue-i18n compiles messages just-in-time (`__INTLIFY_JIT_COMPILATION__`), and a compile error is **thrown**, not warned — it tears down whatever component asked for the message. The translation pipeline had been localizing placeholder *names*, which is exactly that kind of error: ``` en-US "Are you sure you want to delete {identifier}?" zh-CN "您确定要删除 {标识符} 吗?" -> SyntaxError: 2 (INVALID_TOKEN_IN_PLACEHOLDER) ``` IAM → Service Accounts rendered blank in Chinese for this reason. The two zh-CN strings named in the issue had already been patched by hand in #13707, but the *class* was never swept. ## What was actually broken Compiling every message in every locale file with vue-i18n's own compiler found three groups: | | count | symptom | |---|---|---| | Does not compile | 132 | page blanks with `SyntaxError` — 8 locales: es, fr, it, ja, ko, nl, pt, tr | | Compiles, unknown name | 147 | `{Anzahl}` for `{count}` → value renders as an empty string | | Stale text, dropped placeholder | 5 | `{org}`, `{payer}`, `{endpoint}`, `{service}` — English source no longer has them | Worst of the first group are mangled vue-i18n literal escapes, e.g. `{'{'}team{'}'}` coming back as `{'{'} '}` — an unbalanced brace. All 279 are corrected here. The existing translated prose is kept verbatim; only the tokens are restored. The 5 stale ones are retranslated from the current English text, since restoring a token that no longer exists is not an option. ## Why it can't come back **`web/src/locales/enUsMessages.spec.ts` already did this — for en-US only.** That left the machine-translated locales, the actual source of the bug, uncovered. Renamed to `localeMessages.spec.ts` and extended to every locale, plus a second assertion that a translation only interpolates names en-US defines. That second check is a **subset** rule, not equality: dropping a placeholder the target language does not need stays legal (an English plural suffix like `{plural}` has no Japanese equivalent). Introducing a name en-US never passes a value for is always a bug. Placeholder names are collected by *running* the compiled message against a recording context rather than by pattern-matching the source, because only the compiler knows which braces are placeholders and which are literal escapes — `{'}'}` is one token whose body is the character a `{...}` regex stops at. All plural branches are evaluated. **`scripts/translations/translator.py` — `_validate` had the same regex blind spot** (`{[^{}]*}`) and never checked that the output compiles at all, which is why these landed. It now tokenizes the way vue-i18n does and rejects anything vue-i18n would refuse, so the pipeline stops emitting them rather than CI merely catching them afterwards. Escape drift (`{'{'} rows {'} rows {'}`) is caught too, because literal escapes are now part of the compared token multiset. ## Verification - The Python scanner was cross-checked against vue-i18n's compiler over **all 170,055 messages** in the 15 locale files — identical compile verdicts and identical token sets, 0 divergences. - Injecting the original `{标识符}` string back into zh-CN fails the new spec; injecting a compiling-but-wrong `{bilv}` fails the second assertion. - `npx vitest run --environment jsdom --root src/ locales/` → 45 passed. `prettier --check` and `tsc` clean on the spec. - Only `en-US.json` is imported anywhere in `web/src`, so no existing test depends on the changed locale data. ## Not in scope A handful of translations are awkward but compile and bind correctly (e.g. ko-KR renders "ingestion limit" with the eating sense of the word). Left alone to keep this diff auditable as a mechanical token restoration. --- scripts/translations/README.md | 12 +- scripts/translations/translator.py | 111 +++++++++++++-- web/src/locales/enUsMessages.spec.ts | 53 ------- web/src/locales/languages/de-DE.json | 44 +++--- web/src/locales/languages/es-ES.json | 46 +++--- web/src/locales/languages/fr-FR.json | 74 +++++----- web/src/locales/languages/it-IT.json | 36 ++--- web/src/locales/languages/ja-JP.json | 90 ++++++------ web/src/locales/languages/ko-KR.json | 118 ++++++++-------- web/src/locales/languages/nl-NL.json | 40 +++--- web/src/locales/languages/pt-PT.json | 62 ++++---- web/src/locales/languages/tr-TR.json | 52 +++---- web/src/locales/localeMessages.spec.ts | 187 +++++++++++++++++++++++++ 13 files changed, 582 insertions(+), 343 deletions(-) delete mode 100644 web/src/locales/enUsMessages.spec.ts create mode 100644 web/src/locales/localeMessages.spec.ts diff --git a/scripts/translations/README.md b/scripts/translations/README.md index fc490dc987..df91867217 100644 --- a/scripts/translations/README.md +++ b/scripts/translations/README.md @@ -75,10 +75,20 @@ RTL languages (Arabic, Persian) are deliberately excluded until the web app has 1. **Source File**: All translations originate from `web/src/locales/languages/en-US.json` 2. **Translation**: New/changed keys are translated by DeepSeek, in batches, with - interpolation placeholders (`{count}`, `%s`, `@:linked.key`) validated per string + interpolation placeholders (`{count}`, `%s`, `@:linked.key`) and vue-i18n + literal escapes (`{'{'}`, `{'@'}`) validated per string 3. **Preservation**: Existing translations are never overwritten unless their English source changed 4. **Nested Support**: Handles nested JSON structures correctly +A translation is rejected (and retried on the next run) when it is empty, drops or +adds an interpolation token, changes the `|` plural-form count, or is something +vue-i18n cannot compile. That last check exists because vue-i18n compiles messages +just-in-time and a compile error is *thrown*, not warned: a model that helpfully +localises a placeholder name — `{identifier}` → `{标识符}` — blanks every page that +renders the string. `web/src/locales/localeMessages.spec.ts` re-checks the same +property against the committed locale files, so a bad string cannot reach `main` +even if it predates this validation. + ## Local Development ### Prerequisites diff --git a/scripts/translations/translator.py b/scripts/translations/translator.py index 7e218277eb..1e1ced90fd 100644 --- a/scripts/translations/translator.py +++ b/scripts/translations/translator.py @@ -72,9 +72,23 @@ LANGUAGE_NAMES = { # Add them here together with RTL layout support and their web wiring. } -# vue-i18n / printf style interpolation tokens that MUST survive translation -# unchanged: {count}, {name}, {0}, %s, %d, @:linked.key -_PLACEHOLDER = re.compile(r"{[^{}]*}|%[sd]|@:[\w.]+") +# printf / linked-message tokens that MUST survive translation unchanged. +# Brace tokens are handled by _scan_tokens, which a regex cannot do: `{'}'}` is a +# single literal-escape token whose body contains the very character a regex like +# `{[^{}]*}` stops at. +_PRINTF = re.compile(r"%[sd]") +# `@` always starts a linked message in vue-i18n — `@:key` or `@.modifier:key`. +# A bare `@` (even inside a word, e.g. an email address) is a compile error; the +# locale files spell it `{'@'}` for that reason. +_LINKED = re.compile(r"@(?:\.[A-Za-z_$][A-Za-z0-9_$]*)?:[\w.]+") + +# A vue-i18n named placeholder is a JS-style identifier; a list placeholder is an +# index. Anything else — most importantly a name the model helpfully translated, +# `{标识符}` for `{identifier}` — is INVALID_TOKEN_IN_PLACEHOLDER, and vue-i18n +# compiles messages just-in-time, so it *throws* at render time and blanks the +# page that used the string. +_NAMED = re.compile(r"[A-Za-z_$][A-Za-z0-9_$]*\Z") +_LIST = re.compile(r"\d+\Z") _client = None # Batches are translated concurrently (see translate_pending), so the lazy client @@ -157,9 +171,83 @@ def new_counters(): return {"pending": 0, "kept": 0, "translated": 0, "failed": 0, "failed_paths": set()} +def _scan_tokens(text): + """ + Return (tokens, compilable) for one message. + + `tokens` is the multiset of interpolation tokens vue-i18n would see — + placeholders (`{count}`, `{0}`), literal escapes (`{'{'}`, `{'@'}`), printf + tokens and linked-message references — normalised so that incidental + whitespace inside braces does not count as a difference. + + `compilable` is False when the message is something vue-i18n's compiler would + reject outright: a placeholder whose name is not an identifier, an unclosed + or unbalanced brace, or an unterminated literal escape. Those are what turn a + translation into a thrown SyntaxError at render time instead of a wrong-looking + label, so they must never be written to a locale file. + """ + tokens = [] + i = 0 + n = len(text) + while i < n: + ch = text[i] + + if ch == "%": + m = _PRINTF.match(text, i) + if m: + tokens.append(m.group(0)) + i = m.end() + continue + + if ch == "@": + m = _LINKED.match(text, i) + if not m: + return tokens, False # bare @ — must be written {'@'} + tokens.append(m.group(0)) + i = m.end() + continue + + if ch == "}": + return tokens, False # closing brace with nothing open + + if ch != "{": + i += 1 + continue + + j = i + 1 + while j < n and text[j].isspace(): + j += 1 + + if j < n and text[j] == "'": + # Literal escape: {'{'}, {'}'}, {'@'}, {'|'}. The body is quoted, so + # scan to the closing quote rather than to the next brace. + end = text.find("'", j + 1) + if end == -1: + return tokens, False + k = end + 1 + while k < n and text[k].isspace(): + k += 1 + if k >= n or text[k] != "}": + return tokens, False + tokens.append("{'" + text[j + 1 : end] + "'}") + i = k + 1 + continue + + end = text.find("}", j) + if end == -1: + return tokens, False + name = text[j:end].strip() + if not (_NAMED.match(name) or _LIST.match(name)): + return tokens, False + tokens.append("{" + name + "}") + i = end + 1 + + return tokens, True + + def _placeholders(text): """Multiset of interpolation tokens in a string (order-independent).""" - return sorted(_PLACEHOLDER.findall(text)) + return sorted(_scan_tokens(text)[0]) def _get_client(): @@ -442,13 +530,20 @@ def _validate(src, out): Return `out` if it safely preserves `src`'s structure, else None. Rejects a translation that is empty/whitespace-only (which would blank the - label), changes the interpolation-placeholder multiset ({count}, %s, @:key), - or changes the vue-i18n pluralization pipe count (`|`) — each silently breaks - runtime rendering. + label), that vue-i18n could not compile at all, that changes the + interpolation-token multiset ({count}, {'{'}, %s, @:key), or that changes the + vue-i18n pluralization pipe count (`|`). + + The compilability check is the one that matters most: a translated placeholder + name or a mangled literal escape makes vue-i18n throw while rendering, which + blanks every component on the page rather than just spoiling one label. """ if not isinstance(out, str) or not out.strip(): return None - if _placeholders(src) != _placeholders(out): + out_tokens, compilable = _scan_tokens(out) + if not compilable: + return None + if _placeholders(src) != sorted(out_tokens): return None if src.count("|") != out.count("|"): return None diff --git a/web/src/locales/enUsMessages.spec.ts b/web/src/locales/enUsMessages.spec.ts deleted file mode 100644 index bb0bc4cabf..0000000000 --- a/web/src/locales/enUsMessages.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2026 OpenObserve Inc. -// -// vue-i18n compiles a message the first time it is rendered (JIT compilation is -// on — see __INTLIFY_JIT_COMPILATION__ in vite.config.ts). A message whose text -// contains raw `{`, `}` or `@` is not a string to vue-i18n: it is interpolation -// or linked-message syntax, and compiling it throws. The throw happens inside -// render, so it does not surface as a nice error — it tears down whatever -// component asked for the message. -// -// That is invisible until the exact branch that renders the message runs. A -// `{"key": "value"}` placeholder on the HTTP check's request-body field only -// renders for non-GET methods, so picking POST made the entire HTTP Request -// card disappear with nothing but `SyntaxError: 2` in the console. -// -// Compiling every en-US message here turns that into a test failure instead. -// Literal braces/at-signs must be escaped as {'{'}, {'}'}, {'@'}. - -import { describe, expect, it } from "vitest"; -import { baseCompile } from "@intlify/message-compiler"; -import enUS from "./languages/en-US.json"; - -function flatten(node: unknown, path = "", out: [string, string][] = []): [string, string][] { - if (typeof node === "string") { - out.push([path, node]); - return out; - } - if (node && typeof node === "object") { - for (const [k, v] of Object.entries(node)) { - flatten(v, path ? `${path}.${k}` : k, out); - } - } - return out; -} - -describe("en-US messages", () => { - it("should all compile with vue-i18n", () => { - const broken: string[] = []; - for (const [key, value] of flatten(enUS)) { - try { - // onError rethrows: baseCompile otherwise collects errors and returns a - // best-effort AST, which would hide exactly what we are looking for. - baseCompile(value, { - onError: (err) => { - throw err; - }, - }); - } catch (err) { - broken.push(`${key}: ${JSON.stringify(value)} — ${String(err).split("\n")[0]}`); - } - } - expect(broken).toEqual([]); - }); -}); diff --git a/web/src/locales/languages/de-DE.json b/web/src/locales/languages/de-DE.json index 259ce15f9b..e51872d157 100644 --- a/web/src/locales/languages/de-DE.json +++ b/web/src/locales/languages/de-DE.json @@ -387,8 +387,8 @@ "description": "Einladungen, die Sie gesendet oder erhalten haben, werden hier angezeigt." }, "filtered": { - "title": "Kein {Substantiv} gefunden", - "description": "Kein {Substantiv} entspricht Ihren aktuellen Filtern oder Ihrer Suche. Versuchen Sie, sie anzupassen oder zu löschen.", + "title": "Kein {noun} gefunden", + "description": "Kein {noun} entspricht Ihren aktuellen Filtern oder Ihrer Suche. Versuchen Sie, sie anzupassen oder zu löschen.", "action": "Filter löschen", "actionDesc": "Alle Filter zurücksetzen und suchen", "fallbackNoun": "Ergebnisse" @@ -571,7 +571,7 @@ "attributes": "Eigenschaften", "events": "Ereignisse", "goHome": "Zur Startseite", - "redirectingHome": "In {Countdown} Sekunden wird zur Startseite weitergeleitet...", + "redirectingHome": "In {countdown} Sekunden wird zur Startseite weitergeleitet...", "name": "Name", "add": "Hinzufügen", "selectAll": "Alle auswählen", @@ -1115,12 +1115,12 @@ "trendFlat": "Wohnung", "trendNoData": "Keine Vorkommen im ausgewählten Zeitraum", "eventsUnit": "Veranstaltungen", - "firstSeenAgo": "erstes {mal}", + "firstSeenAgo": "erstes {time}", "errorsOverTime": "Fehler im Laufe der Zeit", "noErrorsInWindow": "Keine Fehler im ausgewählten Zeitraum", "unhandled": "Unbehandelt", "handled": "Behandelt", - "deployMarker": "▲ {Version} bereitstellen", + "deployMarker": "▲ {version} bereitstellen", "spikeAfterDeploy": "{factor} × Anstieg direkt nach der Bereitstellung von {version}", "errorKpisAria": "Zusammenfassung der Fehlermetriken", "totalErrors": "Fehler insgesamt", @@ -1220,7 +1220,7 @@ "activity": "Aktivität", "noActivityBounced": "Keine Aktivität — ist sofort abgeprallt", "eventsCount": "{count} Ereignisse", - "activityAriaLabel": "{events} Ereignisse, {errors} Fehler, {Frustrationen} Frustrationen während der Sitzungsdauer", + "activityAriaLabel": "{events} Ereignisse, {errors} Fehler, {frustrations} Frustrationen während der Sitzungsdauer", "timestamp": "Zeitstempel", "type": "Typ", "viewURL": "URL anzeigen", @@ -1724,7 +1724,7 @@ "confirmRejectMsg": "Bist du sicher, dass du die Einladung zum Beitritt zu {org} ablehnen möchtest?", "expired": "Abgelaufen", "expiresToday": "Läuft heute ab", - "daysLeft": "{Tage} verbleibende Tage" + "daysLeft": "{days} verbleibende Tage" }, "serviceAccounts": { "header": "Dienstkonten", @@ -2132,7 +2132,7 @@ "orgBeingDeletedSwitching": "Diese Organisation wird gelöscht. Sie werden zu einer anderen Organisation umgeleitet.", "createOrganization": "Organisation erstellen", "updateOrganization": "Organisation aktualisieren", - "makeBilledMember": "Diese Organisation zu einem Rechnungsmitglied von {org} machen", + "makeBilledMember": "Diese Organisation als Rechnungsmitglied der aktuellen Organisation hinzufügen.", "add": "Organisation hinzufügen", "save": "Speichern", "cancel": "Abbrechen", @@ -3784,7 +3784,7 @@ "maximum": "Maximal", "fixedValue": "Fester Wert", "legendLabel": "Legende", - "overrideMessage": "Überschreibungen des Seriennamens. Beispielsweise muss {endpoint} durch den Labelwert für Endpunkt ersetzt werden.", + "overrideMessage": "Überschreibungen des Seriennamens.", "overrideMessageExample": "Beispielsweise wird {endpoint} durch den Labelwert für Endpunkt ersetzt.", "unsavedMessage": "Sie haben ungespeicherte Änderungen. Bist du sicher, dass du gehen willst?", "addFieldMessage": "Bitte fügen Sie ein Feld aus der Liste hinzu", @@ -4834,7 +4834,7 @@ "recentEvents": "Aktuelle Ereignisse", "investigate": "Untersuchen →", "viewAll": "Alles ansehen", - "showingOf": "Es werden {gezeigt} von {total} angezeigt", + "showingOf": "Es werden {shown} von {total} angezeigt", "colErrorRate": "Fehlerrate", "colLatency": "Latenz", "colReqs": "Requen", @@ -5089,7 +5089,7 @@ "inviteAccepted": "Einladung erfolgreich angenommen", "inviteRejected": "Einladung erfolgreich abgelehnt", "plansManagedTitle": "Abrechnung wird von der Mutterorganisation verwaltet", - "plansManagedDescription": "Die Abrechnung dieser Organisation wird von {payer} verwaltet. Planänderungen werden von der Mutterorganisation bearbeitet.", + "plansManagedDescription": "Die Abrechnung dieser Organisation wird von {name} ({id}) verwaltet. Planänderungen werden von der Mutterorganisation bearbeitet.", "chipPlanManaged": "Vom Elternteil verwalteter Plan", "chipNoInvoices": "Keine separaten Rechnungen", "viewOrgGroup": "Organisationsgruppe anzeigen", @@ -5884,7 +5884,7 @@ "currentWindow": "Aktuelles Fenster", "cycle": "Zyklus", "cycleTooltip": "Vergleichen Sie die Ergebnisse mit der gleichen Zeit im vorherigen Zyklus.", - "runningFor": "Läuft für {Zeitraum} im Intervall jeder {Frequenz}", + "runningFor": "Läuft für {period} im Intervall jeder {frequency}", "currentWindowCannotBeDeleted": "Das aktuelle Fenster kann nicht gelöscht werden", "comparingWith": "Im Vergleich zu", "referenceWindow": "Referenzfenster", @@ -7952,7 +7952,7 @@ "searchPlaceholder": "Suchen Sie in dieser Sitzung nach Spuren...", "turn": "Wende", "turns": "wendet sich", - "turnCount": "{Anzahl} {Einheit}", + "turnCount": "{count} {unit}", "openInTraceExplorer": "Trace ansehen", "filters": { "status": "Status", @@ -7983,12 +7983,12 @@ "kpiSub": { "turns": "Sitzung abschließen · {duration}", "errorsLead": "{errors} von {total} · Spielzüge", - "errorsLeadMany": "{Fehler} von {total} Runden", + "errorsLeadMany": "{errors} von {total} Runden", "filterErrors": "Fehler filtern", "resetFilters": "Filter zurücksetzen", "errorsNone": "Keine Fehler · {total} Umdrehungen", - "latencyLead": "Langsamste {langsamste} ·", - "costLead": "Gipfel {Gipfel} ·", + "latencyLead": "Langsamste {slowest} ·", + "costLead": "Gipfel {peak} ·", "tokens": "{ratio}% Zwischengespeichert", "cacheImpact": "{ratio}% Wiederverwendet · {tokens} Zu lesen", "cacheEstimate": "Zu den Nettoauswirkungen gehören Cache-Lesevorgänge und Kosten für die Cache-Erstellung.", @@ -8055,7 +8055,7 @@ "external": "Extern", "rpc": "RPC" }, - "unhealthyTooltip": "{Anzahl} {Status}", + "unhealthyTooltip": "{count} {status}", "legend": { "title": "Fehlerrate" } @@ -8676,7 +8676,7 @@ "dimensionBasedCorrelation": "Dimensionsbasierte Korrelation", "tracesFromService": "Spuren vom Dienst: {service}", "noTracesFound": "Keine korrelierten Spuren gefunden", - "noTracesDescription": "Keine Spuren für Service {service} im ausgewählten Zeitraum gefunden", + "noTracesDescription": "Für diese Korrelation wurden im ausgewählten Zeitraum keine Traces gefunden.", "viewInTraces": "In Traces ansehen", "filters": "Filter", "semanticFieldGroupsCaption": "Definieren Sie Feldnamenzuordnungen für Korrelation und Deduplizierung (z. B. Host, Hostname, Knoten → Hostgruppe)", @@ -8904,7 +8904,7 @@ "enterModelName": "Geben Sie einen Modellnamen ein, um den Abgleich zu testen", "clickToTest": "Klicken Sie auf „Spiel testen“, um die Ergebnisse zu sehen", "noMatchFound": "Keine Übereinstimmung gefunden", - "noMatchDesc": "Keine Regel stimmte mit „{ModelName}“ überein.", + "noMatchDesc": "Keine Regel stimmte mit „{modelName}“ überein.", "troubleshootingTitle": "Tipps zur Problembehebung:", "tip1": "Prüfen Sie die Schreibweise Ihres Modellnamens", "tip2": "Stellen Sie sicher, dass eine Regel mit einem passenden Regex-Muster existiert und aktiviert ist", @@ -9721,7 +9721,7 @@ }, "drillTitle": "Auf Spuren bohren", "trendTitle": "Trend", - "trendSubtitle": "{ConfigName}, {bucket}", + "trendSubtitle": "{configName}, {bucket}", "distributionTitle": "Verteilung der Punktzahlen", "compositionTitle": "Zusammensetzung im Laufe der Zeit", "categoryDistributionTitle": "Kategorie Vertrieb", @@ -9731,7 +9731,7 @@ "trueFalseTitle": "Wahr gegen Falsch", "legendAvg": "Durchschn.", "legendP95": "p95", - "legendThreshold": "Schwellenwert {Richtung} {Wert}", + "legendThreshold": "Schwellenwert {direction} {value}", "legendHealthy": "gesund", "legendUnhealthy": "ungesund", "legendHealthyRate": "Gesundheitsrate", @@ -10466,7 +10466,7 @@ "trace": "Wählen Sie einen oder mehrere Scorer aus, die für jede berechtigte Trace ausgeführt werden sollen.", "session": "Wählen Sie einen oder mehrere Scorer aus, die für jede berechtigte Sitzung ausgeführt werden sollen." }, - "meta": "{Typ} · v {Version}", + "meta": "{type} · v{version}", "placeholder": "Scorer auswählen", "searchPlaceholder": "Scorer suchen…", "empty": "Keine Scorer verfügbar. Erstellen Sie zuerst einen im Tab „Scorer“." diff --git a/web/src/locales/languages/es-ES.json b/web/src/locales/languages/es-ES.json index 77d80108fc..54e49f14cb 100644 --- a/web/src/locales/languages/es-ES.json +++ b/web/src/locales/languages/es-ES.json @@ -840,7 +840,7 @@ "patternAnomalyRarePlural": "Este patrón se detecta como un patrón poco frecuente ({pct}% de registros, {freq} ocurrencias)", "patternAnomalyLowFreq": "Este patrón aparece con menos frecuencia de lo esperado ({freq} vs avg {avg}, z-score: {z})", "patternAnomalyLowFreqPlural": "Este patrón aparece con menos frecuencia de lo esperado ({freq} vs avg {avg}, z-score: {z})", - "patternZScore": "Puntuación Z: {zScore} (frecuencia promedio: {AvgFrequency})", + "patternZScore": "Puntuación Z: {zScore} (frecuencia promedio: {avgFrequency})", "patternVariablesHeader": "Variables", "patternVariablesDetected": "{count} variable detectada | {count} variables detectadas", "patternNoVariablesDetected": "No se detectaron variables", @@ -1115,12 +1115,12 @@ "trendFlat": "plano", "trendNoData": "No hay ocurrencias en el intervalo de tiempo seleccionado", "eventsUnit": "eventos", - "firstSeenAgo": "primera {vez}", + "firstSeenAgo": "primera {time}", "errorsOverTime": "Errores a lo largo del tiempo", "noErrorsInWindow": "No hay errores en el intervalo de tiempo seleccionado", "unhandled": "Sin control", "handled": "Manejado", - "deployMarker": "▲ {versión} implementar", + "deployMarker": "▲ {version} implementar", "spikeAfterDeploy": "{factor} × pico justo después de la implementación de {version}", "errorKpisAria": "Resumen de métricas de errores", "totalErrors": "Total de errores", @@ -1220,7 +1220,7 @@ "activity": "Actividad", "noActivityBounced": "Sin actividad: se rebotó inmediatamente", "eventsCount": "{count} eventos", - "activityAriaLabel": "{eventos} eventos, {errores} errores, {frustraciones} frustraciones durante la sesión", + "activityAriaLabel": "{events} eventos, {errors} errores, {frustrations} frustraciones durante la sesión", "timestamp": "Marca de tiempo", "type": "Tipo", "viewURL": "Ver URL", @@ -1724,7 +1724,7 @@ "confirmRejectMsg": "¿Estás seguro de que quieres rechazar la invitación para unirte a {org}?", "expired": "Expirado", "expiresToday": "Caduca hoy", - "daysLeft": "Quedan {días} días" + "daysLeft": "Quedan {days} días" }, "serviceAccounts": { "header": "Cuentas de servicio", @@ -2132,7 +2132,7 @@ "orgBeingDeletedSwitching": "Esta organización está siendo eliminada. Cambiándolo a otra organización.", "createOrganization": "Crear organización", "updateOrganization": "Actualizar organización", - "makeBilledMember": "Haga de esta organización un miembro de facturación de {org}", + "makeBilledMember": "Añadir esta organización como miembro de facturación de la organización actual.", "add": "Agregar organización", "save": "Guardar", "cancel": "Cancelar", @@ -3784,7 +3784,7 @@ "maximum": "Máximo", "fixedValue": "Valor fijo", "legendLabel": "Leyenda", - "overrideMessage": "El nombre de la serie se anula. Por ejemplo, {endpoint} se sustituirá por el valor de etiqueta para el punto final.", + "overrideMessage": "Anulaciones del nombre de la serie.", "overrideMessageExample": "Por ejemplo, {endpoint} se reemplazará por el valor de etiqueta de endpoint.", "unsavedMessage": "Tienes cambios sin guardar. ¿Estás seguro de que quieres salir?", "addFieldMessage": "Agregue un campo de la lista", @@ -4834,7 +4834,7 @@ "recentEvents": "Eventos recientes", "investigate": "Investiga →", "viewAll": "Ver todos", - "showingOf": "Mostrando {mostrado} de {total}", + "showingOf": "Mostrando {shown} de {total}", "colErrorRate": "Tasa de error", "colLatency": "Latencia", "colReqs": "Reqes", @@ -5089,7 +5089,7 @@ "inviteAccepted": "La invitación se ha aceptado correctamente", "inviteRejected": "Invitación rechazada correctamente", "plansManagedTitle": "Facturación gestionada por la organización matriz", - "plansManagedDescription": "La facturación de esta organización la gestiona {payer}. Los cambios de plan son gestionados por la organización matriz.", + "plansManagedDescription": "La facturación de esta organización la gestiona {name} ({id}). Los cambios de plan son gestionados por la organización matriz.", "chipPlanManaged": "Plan administrado por el padre", "chipNoInvoices": "Sin facturas separadas", "viewOrgGroup": "Ver grupo organizativo", @@ -5884,7 +5884,7 @@ "currentWindow": "Ventana actual", "cycle": "Ciclo", "cycleTooltip": "Compare los resultados con los del mismo tiempo del ciclo anterior.", - "runningFor": "Se ejecuta durante {período} en el intervalo de cada {frecuencia}", + "runningFor": "Se ejecuta durante {period} en el intervalo de cada {frequency}", "currentWindowCannotBeDeleted": "No se puede eliminar la ventana actual", "comparingWith": "Comparando con", "referenceWindow": "Ventana de referencia", @@ -5951,10 +5951,10 @@ "helpWhyWithoutCode": "«text»: «Gravedad: alta — depende de los pagos»", "helpWhyWithLabel": "Con una variable", "helpWhyWithDesc": "Una plantilla compartida hace referencia a la variable; cada alerta establece su propio valor aquí.", - "helpWhyWithCode": "«text»: «Gravedad: {'{'} severidad {'}'} — propiedad del equipo {'{'} '}»", + "helpWhyWithCode": "«text»: «Gravedad: {'{'} severidad {'}'} — propiedad del equipo {'{'} team {'}'}»", "helpExampleHeading": "Ejemplo", "helpVariablesExampleCaption": "Añade la gravedad y el equipo como variables y, a continuación, úsalas en cualquier parte de la plantilla:", - "helpVariablesExampleCode": "{'{'}\n «text»: «[{'{'} severidad {'}'}] {'{'} alert_name {'}'} — {'{'} alert_count {'}'} eventos\\nPropiedad del equipo {'{'} '}»\n{'}'}", + "helpVariablesExampleCode": "{'{'}\n «text»: «[{'{'} severidad {'}'}] {'{'} alert_name {'}'} — {'{'} alert_count {'}'} eventos\\nPropiedad del equipo {'{'} team {'}'}»\n{'}'}", "helpExampleResultLabel": "Qué se envía", "helpVariablesExampleResult": "{'{'} «text»: «[alta] CPU alta: 42 eventos\\nPropiedad de los pagos» {'}'}", "helpTemplateWhenHeading": "¿Cuándo debo anular?", @@ -7920,7 +7920,7 @@ "sessions": "sesiones", "failedToLoad": "No se pudieron cargar las sesiones", "retry": "Reintentar", - "tokenTooltip": "Entrada: {entrada} · Salida: {salida} · Total: {total}", + "tokenTooltip": "Entrada: {input} · Salida: {output} · Total: {total}", "durationNs": "ns", "unknownUser": "Usuario desconocido", "columns": { @@ -7952,7 +7952,7 @@ "searchPlaceholder": "Busque rastros en esta sesión...", "turn": "giro", "turns": "giros", - "turnCount": "{count} {unidad}", + "turnCount": "{count} {unit}", "openInTraceExplorer": "Ver Trace", "filters": { "status": "Estado", @@ -7982,13 +7982,13 @@ }, "kpiSub": { "turns": "Sesión completa · {duration}", - "errorsLead": "{errores} de {total} · Turnos", - "errorsLeadMany": "{errores} de {total} turnos", + "errorsLead": "{errors} de {total} · Turnos", + "errorsLeadMany": "{errors} de {total} turnos", "filterErrors": "Filtrar errores", "resetFilters": "Restablecer filtros", "errorsNone": "Sin errores · {total} turnos", - "latencyLead": "El más lento {el más lento} ·", - "costLead": "Pico {pico} ·", + "latencyLead": "El más lento {slowest} ·", + "costLead": "Pico {peak} ·", "tokens": "{ratio}% en caché", "cacheImpact": "{ratio}% reutilizado · {tokens} No leído", "cacheEstimate": "El impacto neto incluye las lecturas de la memoria caché y los costos de creación de la memoria caché.", @@ -8055,7 +8055,7 @@ "external": "Externo", "rpc": "RPC" }, - "unhealthyTooltip": "{contar} {estado}", + "unhealthyTooltip": "{count} {status}", "legend": { "title": "Tasa de errores" } @@ -8676,7 +8676,7 @@ "dimensionBasedCorrelation": "Correlación basada en dimensiones", "tracesFromService": "Rastros del servicio: {service}", "noTracesFound": "No se han encontrado rastros correlacionados", - "noTracesDescription": "No se han encontrado rastros del servicio {service} en el intervalo de tiempo seleccionado", + "noTracesDescription": "No se han encontrado trazas para esta correlación en el intervalo de tiempo seleccionado.", "viewInTraces": "Ver en Traces", "filters": "Filtros", "semanticFieldGroupsCaption": "Defina las asignaciones de nombres de campo para la correlación y la deduplicación (por ejemplo, host, nombre de host, nodo → grupo de «anfitriones»)", @@ -9731,7 +9731,7 @@ "trueFalseTitle": "Verdadero contra falso", "legendAvg": "Promedio", "legendP95": "p95", - "legendThreshold": "umbral {dirección} {valor}", + "legendThreshold": "umbral {direction} {value}", "legendHealthy": "saludable", "legendUnhealthy": "insalubres", "legendHealthyRate": "Tasa saludable", @@ -10466,7 +10466,7 @@ "trace": "Selecciona uno o más scorers para ejecutar por cada trace elegible.", "session": "Selecciona uno o más scorers para ejecutar por cada sesión elegible." }, - "meta": "{tipo} · v {versión}", + "meta": "{type} · v{version}", "placeholder": "Seleccionar evaluadores", "searchPlaceholder": "Buscar evaluadores…", "empty": "No hay evaluadores disponibles. Crea uno primero en la pestaña Evaluadores." @@ -10950,7 +10950,7 @@ }, "refreshButton": { "justNow": "justo ahora", - "secondsAgo": "Hace {segundos}", + "secondsAgo": "Hace {sec}", "minutesAgo": "Hace {min} m", "hoursAgo": "Hace {h} h", "notYetRefreshed": "Aún no se ha actualizado", diff --git a/web/src/locales/languages/fr-FR.json b/web/src/locales/languages/fr-FR.json index ee6549bdc3..42f95a71a6 100644 --- a/web/src/locales/languages/fr-FR.json +++ b/web/src/locales/languages/fr-FR.json @@ -387,8 +387,8 @@ "description": "Les invitations que vous avez envoyées ou reçues s'afficheront ici." }, "filtered": { - "title": "Aucun {nom} trouvé", - "description": "Aucun {nom} ne correspond à vos filtres ou recherches actuels. Essayez de les ajuster ou de les effacer.", + "title": "Aucun {noun} trouvé", + "description": "Aucun {noun} ne correspond à vos filtres ou recherches actuels. Essayez de les ajuster ou de les effacer.", "action": "Effacer les filtres", "actionDesc": "Réinitialiser tous les filtres et effectuer une recherche", "fallbackNoun": "résultats" @@ -571,7 +571,7 @@ "attributes": "Attributs", "events": "Évènements", "goHome": "Rentrez chez vous", - "redirectingHome": "Redirection vers la page d'accueil dans {compte à rebours} secondes...", + "redirectingHome": "Redirection vers la page d'accueil dans {countdown} secondes...", "name": "Nom", "add": "Ajouter", "selectAll": "Tout sélectionner", @@ -829,7 +829,7 @@ "occurrenceColumnHeader": "Événement", "anomalyLabel": "ANOMALIE", "patternDetailsTitle": "Détails du motif", - "patternXofY": "Modèle {indice} de {total}", + "patternXofY": "Modèle {index} de {total}", "patternXofYShort": "{index} de {total}", "patternStatistics": "Statistiques", "patternOccurrences": "Évènements", @@ -840,7 +840,7 @@ "patternAnomalyRarePlural": "Ce modèle est détecté comme étant rare ({pct} % des journaux, {freq} occurrences)", "patternAnomalyLowFreq": "Ce schéma apparaît moins fréquemment que prévu ({freq} vs avg {avg}, z-score : {z})", "patternAnomalyLowFreqPlural": "Ce schéma apparaît moins fréquemment que prévu ({freq} vs avg {avg}, z-score : {z})", - "patternZScore": "Score Z : {zScore} (fréquence moyenne : {AVGFrequency})", + "patternZScore": "Score Z : {zScore} (fréquence moyenne : {avgFrequency})", "patternVariablesHeader": "Variables", "patternVariablesDetected": "{count} variable détectée | {count} variables détectées", "patternNoVariablesDetected": "Aucune variable détectée", @@ -1021,7 +1021,7 @@ "editor": "Rédacteur", "addToDashboard": "Ajouter au tableau de bord", "selectStream": "Sélectionnez Stream", - "pattern_summary": "Affichage de 1 à 50 événements sur {TotalEvents} et des modèles {PatternsFound} trouvés dans les événements {LogsAnalyzed} en {TotalTime} ms.", + "pattern_summary": "Affichage de 1 à 50 événements sur {totalEvents} et des modèles {patternsFound} trouvés dans les événements {logsAnalyzed} en {totalTime} ms.", "histogramTitle": "Affichage de {start} à {end} sur {total} événements en {took} ms.", "histogramTitleWithScan": "Affichage de {start} à {end} sur {total} événements en {took} ms. ({scanLabel} : {scanSize})", "recordsChip": "{start} à {end} sur {total} événements", @@ -1115,7 +1115,7 @@ "trendFlat": "appartement", "trendNoData": "Aucune occurrence dans la plage de temps sélectionnée", "eventsUnit": "événements", - "firstSeenAgo": "première {fois}", + "firstSeenAgo": "première {time}", "errorsOverTime": "Erreurs au fil du temps", "noErrorsInWindow": "Aucune erreur dans la plage de temps sélectionnée", "unhandled": "Non manipulé", @@ -1126,7 +1126,7 @@ "totalErrors": "Nombre total d'erreurs", "acrossUniqueIssues": "à travers {count} problèmes uniques", "crashFreeSessions": "Sessions sans crash", - "sessionsHitError": "Les sessions {ErrorSessions} de {TotalSessions} rencontrent une erreur", + "sessionsHitError": "Les sessions {errorSessions} de {totalSessions} rencontrent une erreur", "usersAffected": "Utilisateurs concernés", "ofActiveUsers": "de {total} utilisateurs actifs · {pct} %", "newIssues": "Nouveaux numéros", @@ -1199,7 +1199,7 @@ "inTimeRange": "dans la plage de temps sélectionnée", "sessionsWithErrors": "sessions avec une erreur ≥ 1", "rageDeadClicks": "Rage & Dead Clics", - "ofTotal": "{nombre} de {total}", + "ofTotal": "{count} de {total}", "noMatchingSessions": "Aucune session ne correspond aux filtres sélectionnés pendant cette période.", "resetFilters": "Réinitialiser les filtres", "errorCountTooltip": "{count} erreurs au cours de cette session", @@ -1220,7 +1220,7 @@ "activity": "Activité", "noActivityBounced": "Aucune activité — rebond immédiat", "eventsCount": "{count} événements", - "activityAriaLabel": "{événements} événements, {erreurs} erreurs, {frustrations} frustrations pendant la durée de la session", + "activityAriaLabel": "{events} événements, {errors} erreurs, {frustrations} frustrations pendant la durée de la session", "timestamp": "Horodatage", "type": "Type", "viewURL": "Afficher l'URL", @@ -1724,7 +1724,7 @@ "confirmRejectMsg": "Êtes-vous sûr de vouloir rejeter l'invitation à rejoindre {org} ?", "expired": "Expiré", "expiresToday": "Expire aujourd'hui", - "daysLeft": "Il reste {jours} jours" + "daysLeft": "Il reste {days} jours" }, "serviceAccounts": { "header": "Comptes de service", @@ -2132,7 +2132,7 @@ "orgBeingDeletedSwitching": "Cette organisation est en cours de suppression. Vous allez être redirigé vers une autre organisation.", "createOrganization": "Créer une organisation", "updateOrganization": "Mettre à jour l'organisation", - "makeBilledMember": "Faire de cette organisation un membre facturant de {org}", + "makeBilledMember": "Ajouter cette organisation en tant que membre facturant de l'organisation actuelle.", "add": "Ajouter une organisation", "save": "Sauver", "cancel": "Annuler", @@ -3784,7 +3784,7 @@ "maximum": "Maximum", "fixedValue": "Valeur fixe", "legendLabel": "Légende", - "overrideMessage": "Le nom de la série prévaut. Par exemple, {endpoint} doit être remplacé par la valeur d'étiquette pour endpoint.", + "overrideMessage": "Remplacements du nom de la série.", "overrideMessageExample": "Par exemple, {endpoint} sera remplacé par la valeur d'étiquette du point de terminaison.", "unsavedMessage": "Vous avez des modifications non enregistrées. Tu es sûre de vouloir partir ?", "addFieldMessage": "Veuillez ajouter un champ dans la liste", @@ -4834,7 +4834,7 @@ "recentEvents": "Évènements récents", "investigate": "Enquêter →", "viewAll": "Afficher tout", - "showingOf": "Affichage de {affiché} de {total}", + "showingOf": "Affichage de {shown} de {total}", "colErrorRate": "Taux d'erreur", "colLatency": "Latence", "colReqs": "Requéres", @@ -5089,7 +5089,7 @@ "inviteAccepted": "Invitation acceptée avec succès", "inviteRejected": "Invitation rejetée avec succès", "plansManagedTitle": "Facturation gérée par l'organisation mère", - "plansManagedDescription": "La facturation de cette organisation est gérée par {payer}. Les modifications de plan sont gérées par l'organisation mère.", + "plansManagedDescription": "La facturation de cette organisation est gérée par {name} ({id}). Les modifications de plan sont gérées par l'organisation mère.", "chipPlanManaged": "Plan géré par le parent", "chipNoInvoices": "Pas de factures séparées", "viewOrgGroup": "Afficher le groupe organisationnel", @@ -5884,7 +5884,7 @@ "currentWindow": "Fenêtre actuelle", "cycle": "Cycle", "cycleTooltip": "Comparez les résultats avec ceux du cycle précédent à la même période.", - "runningFor": "Courir pendant {période} dans l'intervalle de chaque {fréquence}", + "runningFor": "Courir pendant {period} dans l'intervalle de chaque {frequency}", "currentWindowCannotBeDeleted": "La fenêtre actuelle ne peut pas être supprimée", "comparingWith": "Comparaison avec", "referenceWindow": "Fenêtre de référence", @@ -5963,7 +5963,7 @@ "helpRowTemplateWhyDesc": "Une alerte peut correspondre à de nombreux enregistrements. Le modèle de ligne met en forme un enregistrement ; {'{'} rows {'}'}, puis place chaque enregistrement formaté dans le modèle principal. Vous pouvez ainsi répertorier toutes les correspondances sans écrire de boucle.", "helpRowTemplateExampleCaption": "Chaque ligne correspondante est mise en forme par le modèle de ligne, puis {'{'} rows {'}'} les place toutes dans le modèle principal :", "helpRowTemplateExampleRowLabel": "Modèle de ligne (met en forme un enregistrement)", - "helpRowTemplateExampleRowCode": "{'{'} pod {'}'} : {'{'} niveau {'}'} au {'} _horodatage {'} '}", + "helpRowTemplateExampleRowCode": "{'{'} pod {'}'} : {'{'} niveau {'}'} au {'{'} _timestamp {'}'}", "helpRowTemplateExampleMainLabel": "Modèle principal (utilise {'{'} lignes {'}'})", "helpRowTemplateExampleMainCode": "{'{'} « text » : « {'{'} alert_count {'}'} correspond à :\\n{'{'} lignes {'}'} » {'}'}", "helpRowTemplateExampleResult": "{'{'} « text » : « 2 correspond às:\\nweb-1 : ERREUR à 14:02\\nweb-3 : ERREUR à 14:05\" {'}'}", @@ -7159,12 +7159,12 @@ "domainHint": "Entrez les noms de domaine sans le symbole {at_sign}", "addDomain": "Ajouter un domaine", "domainConfiguredCount": "{count} domaine configuré | {count} domaines configurés", - "allowAllUsersFromDomain": "Autoriser tous les utilisateurs de {domaine}", - "allowOnlySpecificUsers": "Autoriser uniquement des utilisateurs spécifiques de {domaine}", - "blockSpecificUsers": "Bloquer uniquement des utilisateurs spécifiques de {domaine}", + "allowAllUsersFromDomain": "Autoriser tous les utilisateurs de {domain}", + "allowOnlySpecificUsers": "Autoriser uniquement des utilisateurs spécifiques de {domain}", + "blockSpecificUsers": "Bloquer uniquement des utilisateurs spécifiques de {domain}", "blockAllUsersFromDomain": "Bloquer tous les utilisateurs de {domain}", - "allUsersAllowedMessage": "Tous les utilisateurs dont l'adresse e-mail se termine par {domaine} seront autorisés à se connecter.", - "allUsersBlockedMessage": "Tous les utilisateurs SSO externes dont les adresses e-mail se terminent par {domaine} seront bloqués du système (interface utilisateur, API et ingestion).", + "allUsersAllowedMessage": "Tous les utilisateurs dont l'adresse e-mail se termine par {domain} seront autorisés à se connecter.", + "allUsersBlockedMessage": "Tous les utilisateurs SSO externes dont les adresses e-mail se terminent par {domain} seront bloqués du système (interface utilisateur, API et ingestion).", "blockedUsersHint": "Les entrées bloquées s'appliquent uniquement aux identités SSO externes (utilisateurs SSO et comptes de service SSO/Token). Ils annulent toute règle d'autorisation et couvrent l'interface utilisateur, l'API et l'ingestion. Les comptes natifs et créés par API ne sont pas concernés. Supprimez-les plutôt.", "addBlockedEmail": "Bloquer un e-mail", "blockedEmailPlaceholder": "par exemple user {domain}", @@ -7272,7 +7272,7 @@ "clearRegistryDescription": "Cela supprime toutes les lignes de registre des agents GenAI découvertes pour cette organisation. Les nouvelles traces correspondantes rerempliront le registre lors de l'ingestion.", "clearRegistryConfirm": "Effacer le registre", "saved": "Le mappage des agents GenAI a été enregistré avec succès", - "clearRegistrySuccess": "Le registre des agents GenAI a été effacé. Les agents stockés {DeletedCount} et les observations en file d'attente {BufferCount} ont été supprimés.", + "clearRegistrySuccess": "Le registre des agents GenAI a été effacé. Les agents stockés {deletedCount} et les observations en file d'attente {bufferCount} ont été supprimés.", "loadFailed": "Impossible de charger le mappage de l'agent GenAI", "saveFailed": "Impossible d'enregistrer le mappage de l'agent GenAI", "clearRegistryFailed": "Impossible d'effacer le registre des agents GenAI", @@ -7920,7 +7920,7 @@ "sessions": "sessions", "failedToLoad": "Impossible de charger les sessions", "retry": "Réessayez", - "tokenTooltip": "Entrée : {entrée} · Sortie : {sortie} · Total : {total}", + "tokenTooltip": "Entrée : {input} · Sortie : {output} · Total : {total}", "durationNs": "ns", "unknownUser": "Utilisateur inconnu", "columns": { @@ -7952,7 +7952,7 @@ "searchPlaceholder": "Rechercher des traces au cours de cette session...", "turn": "virage", "turns": "tours", - "turnCount": "{count} {unité}", + "turnCount": "{count} {unit}", "openInTraceExplorer": "Afficher Trace", "filters": { "status": "État", @@ -7982,15 +7982,15 @@ }, "kpiSub": { "turns": "Session complète · {duration}", - "errorsLead": "{erreurs} de {total} · Tours", - "errorsLeadMany": "{erreurs} de {total} tours", + "errorsLead": "{errors} de {total} · Tours", + "errorsLeadMany": "{errors} de {total} tours", "filterErrors": "Erreurs de filtre", "resetFilters": "Réinitialiser les filtres", "errorsNone": "Aucune erreur · {total} tours", - "latencyLead": "Le plus lent {le plus lent} ·", - "costLead": "Pic {pic} ·", + "latencyLead": "Le plus lent {slowest} ·", + "costLead": "Pic {peak} ·", "tokens": "{ratio} % mis en cache", - "cacheImpact": "{ratio} % Réutilisé · {jetons} À lire", + "cacheImpact": "{ratio} % Réutilisé · {tokens} À lire", "cacheEstimate": "L'impact net inclut les frais de lecture du cache et de création du cache.", "cacheImpactTooltipTitle": "Répartition des coûts liés au cache", "actualCost": "Coût réel", @@ -8055,7 +8055,7 @@ "external": "Externe", "rpc": "RPC" }, - "unhealthyTooltip": "{count} {statut}", + "unhealthyTooltip": "{count} {status}", "legend": { "title": "Taux d'erreur" } @@ -8676,7 +8676,7 @@ "dimensionBasedCorrelation": "Corrélation basée sur les dimensions", "tracesFromService": "Traces provenant du service : {service}", "noTracesFound": "Aucune trace corrélée n'a été trouvée", - "noTracesDescription": "Aucune trace n'a été trouvée pour le service {service} dans la plage de temps sélectionnée", + "noTracesDescription": "Aucune trace n'a été trouvée pour cette corrélation dans la plage de temps sélectionnée.", "viewInTraces": "Afficher dans Traces", "filters": "Filtres", "semanticFieldGroupsCaption": "Définissez des mappages de noms de champs pour la corrélation et la déduplication (par exemple, hôte, nom d'hôte, nœud → groupe « hôte »)", @@ -8690,7 +8690,7 @@ "fieldNamePlaceholder": "Noms de champs (séparés par des virgules)", "category": "Catégorie", "categoryHint": "Filtrer les groupes par catégorie", - "showingGroups": "Affichage de {FilterGroupLength} sur le total des groupes {LocalGroupLength}", + "showingGroups": "Affichage de {filterGroupLength} sur le total des groupes {localGroupLength}", "deduplicateFields": "Champs de déduplication", "deduplicateFieldTooltip": "Sélectionnez les mappages de champs à utiliser pour la prise d'empreintes digitales de déduplication", "alertDeduplicationMessage": "Les alertes avec les mêmes valeurs pour ces champs seront dédupliquées (par exemple, même empreinte digitale = mêmes valeurs de champs)", @@ -8811,7 +8811,7 @@ "fields": "Champs", "fieldsHint": "Afficher le lien uniquement lorsqu'au moins un champ est présent dans l'enregistrement", "fieldInputPlaceholder": "Entrez le nom du champ et appuyez sur Entrée ou cliquez sur +", - "emptyState": "Aucun lien croisé n'est configuré. Cliquez sur « {AddLabel} » pour en créer un.", + "emptyState": "Aucun lien croisé n'est configuré. Cliquez sur « {addLabel} » pour en créer un.", "update": "Mettre à jour", "add": "Ajouter", "streamCrossLinks": "Streamez des liens croisés", @@ -9731,7 +9731,7 @@ "trueFalseTitle": "Vrai contre faux", "legendAvg": "Moyenne", "legendP95": "page 95", - "legendThreshold": "seuil {direction} {valeur}", + "legendThreshold": "seuil {direction} {value}", "legendHealthy": "en bonne santé", "legendUnhealthy": "mauvais pour la santé", "legendHealthyRate": "Taux de santé", @@ -10950,14 +10950,14 @@ }, "refreshButton": { "justNow": "tout à l'heure", - "secondsAgo": "il y a {secondes}", + "secondsAgo": "il y a {sec}", "minutesAgo": "il y a {min} m", "hoursAgo": "il y a {h} h", "notYetRefreshed": "Pas encore actualisé", "dataFresh": "Les données sont récentes", "dataGettingStale": "Les données deviennent obsolètes", "dataStale": "Les données sont périmées", - "lastRefreshed": "Dernière actualisation : {heure}" + "lastRefreshed": "Dernière actualisation : {time}" }, "synthetics": { "versionPrefix": "v", diff --git a/web/src/locales/languages/it-IT.json b/web/src/locales/languages/it-IT.json index 57346b18df..65600a8938 100644 --- a/web/src/locales/languages/it-IT.json +++ b/web/src/locales/languages/it-IT.json @@ -387,8 +387,8 @@ "description": "Gli inviti che hai inviato o ricevuto verranno visualizzati qui." }, "filtered": { - "title": "Nessun {sostantivo} trovato", - "description": "Nessun {sostantivo} corrisponde ai filtri o alla ricerca correnti. Prova a regolarli o cancellarli.", + "title": "Nessun {noun} trovato", + "description": "Nessun {noun} corrisponde ai filtri o alla ricerca correnti. Prova a regolarli o cancellarli.", "action": "Cancella filtri", "actionDesc": "Reimposta tutti i filtri e cerca", "fallbackNoun": "risultati" @@ -1115,7 +1115,7 @@ "trendFlat": "appartamento", "trendNoData": "Nessuna ricorrenza nell'intervallo di tempo selezionato", "eventsUnit": "eventi", - "firstSeenAgo": "prima {volta}", + "firstSeenAgo": "prima {time}", "errorsOverTime": "Errori nel tempo", "noErrorsInWindow": "Nessun errore nell'intervallo di tempo selezionato", "unhandled": "Non maneggiato", @@ -1724,7 +1724,7 @@ "confirmRejectMsg": "Sei sicuro di voler rifiutare l'invito a partecipare a {org}?", "expired": "Scaduto", "expiresToday": "Scade oggi", - "daysLeft": "{giorni} giorni rimanenti" + "daysLeft": "{days} giorni rimanenti" }, "serviceAccounts": { "header": "Account di servizio", @@ -2132,7 +2132,7 @@ "orgBeingDeletedSwitching": "Questa organizzazione è in fase di eliminazione. Ti stiamo reindirizzando a un'altra organizzazione.", "createOrganization": "Crea organizzazione", "updateOrganization": "Aggiorna organizzazione", - "makeBilledMember": "Rendi questa organizzazione un membro di fatturazione di {org}", + "makeBilledMember": "Aggiungi questa organizzazione come membro di fatturazione dell'organizzazione corrente.", "add": "Aggiungi organizzazione", "save": "Salva", "cancel": "Annulla", @@ -3784,7 +3784,7 @@ "maximum": "Massimo", "fixedValue": "Valore fisso", "legendLabel": "Leggenda", - "overrideMessage": "Sostituisce il nome della serie. Ad esempio, {endpoint} deve essere sostituito con il valore dell'etichetta per endpoint.", + "overrideMessage": "Sostituzioni del nome della serie.", "overrideMessageExample": "Ad esempio, {endpoint} verrà sostituito con il valore dell'etichetta per endpoint.", "unsavedMessage": "Hai delle modifiche non salvate. Sei sicuro di voler andartene?", "addFieldMessage": "Aggiungi un campo dall'elenco", @@ -4834,7 +4834,7 @@ "recentEvents": "Eventi recenti", "investigate": "Indaga →", "viewAll": "Visualizza tutto", - "showingOf": "Mostrando {show} di {total}", + "showingOf": "Mostrando {shown} di {total}", "colErrorRate": "Tasso di errore", "colLatency": "Latenza", "colReqs": "Requs", @@ -5089,7 +5089,7 @@ "inviteAccepted": "Invito accettato con successo", "inviteRejected": "Invito rifiutato con successo", "plansManagedTitle": "Fatturazione gestita dall'organizzazione madre", - "plansManagedDescription": "La fatturazione di questa organizzazione è gestita da {payer}. Le modifiche al piano sono gestite dall'organizzazione madre.", + "plansManagedDescription": "La fatturazione di questa organizzazione è gestita da {name} ({id}). Le modifiche al piano sono gestite dall'organizzazione madre.", "chipPlanManaged": "Piano gestito dal genitore", "chipNoInvoices": "Nessuna fattura separata", "viewOrgGroup": "Visualizza il gruppo organizzativo", @@ -5884,7 +5884,7 @@ "currentWindow": "Finestra corrente", "cycle": "Ciclo", "cycleTooltip": "Confronta i risultati con quelli dello stesso periodo del ciclo precedente.", - "runningFor": "In corso per {periodo} nell'intervallo di ogni {frequenza}", + "runningFor": "In corso per {period} nell'intervallo di ogni {frequency}", "currentWindowCannotBeDeleted": "La finestra corrente non può essere eliminata", "comparingWith": "Confrontando con", "referenceWindow": "Finestra di riferimento", @@ -7952,7 +7952,7 @@ "searchPlaceholder": "Cerca tracce in questa sessione...", "turn": "girare", "turns": "turni", - "turnCount": "{count} {unità}", + "turnCount": "{count} {unit}", "openInTraceExplorer": "Visualizza Trace", "filters": { "status": "Status", @@ -7981,14 +7981,14 @@ "cacheImpact": "Impatto sulla cache" }, "kpiSub": { - "turns": "Sessione completa · {durata}", - "errorsLead": "{errori} di {total} · Turni", - "errorsLeadMany": "{errori} di {total} turni", + "turns": "Sessione completa · {duration}", + "errorsLead": "{errors} di {total} · Turni", + "errorsLeadMany": "{errors} di {total} turni", "filterErrors": "Errori di filtro", "resetFilters": "Reimposta filtri", "errorsNone": "Nessun errore · {total} Turni", - "latencyLead": "Il più lento {il più lento} ·", - "costLead": "Picco {picco} ·", + "latencyLead": "Il più lento {slowest} ·", + "costLead": "Picco {peak} ·", "tokens": "{ratio}% Memorizzato nella cache", "cacheImpact": "{ratio}% Riutilizzato · {tokens} Tok Letto", "cacheEstimate": "L'impatto netto include le letture e i costi di creazione della cache.", @@ -8676,7 +8676,7 @@ "dimensionBasedCorrelation": "Correlazione basata sulle dimensioni", "tracesFromService": "Tracce dal servizio: {service}", "noTracesFound": "Nessuna traccia correlata trovata", - "noTracesDescription": "Nessuna traccia trovata per service {service} nell'intervallo di tempo selezionato", + "noTracesDescription": "Nessuna traccia trovata per questa correlazione nell'intervallo di tempo selezionato.", "viewInTraces": "Visualizza in Tracce", "filters": "Filtri", "semanticFieldGroupsCaption": "Definire le mappature dei nomi dei campi per la correlazione e la deduplicazione (ad esempio, host, hostname, node → gruppo 'host')", @@ -9731,7 +9731,7 @@ "trueFalseTitle": "Vero vs falso", "legendAvg": "Media", "legendP95": "p95", - "legendThreshold": "soglia {direzione} {valore}", + "legendThreshold": "soglia {direction} {value}", "legendHealthy": "salutare", "legendUnhealthy": "malsano", "legendHealthyRate": "Tasso di healthy", @@ -10466,7 +10466,7 @@ "trace": "Seleziona uno o più scorer da eseguire per ogni trace idonea.", "session": "Seleziona uno o più scorer da eseguire per ogni sessione idonea." }, - "meta": "{tipo} · v {versione}", + "meta": "{type} · v{version}", "placeholder": "Seleziona scorer", "searchPlaceholder": "Cerca scorer…", "empty": "Nessuno scorer disponibile. Creane prima uno nella scheda Scorer." diff --git a/web/src/locales/languages/ja-JP.json b/web/src/locales/languages/ja-JP.json index 05beb7f19b..6014d53594 100644 --- a/web/src/locales/languages/ja-JP.json +++ b/web/src/locales/languages/ja-JP.json @@ -829,8 +829,8 @@ "occurrenceColumnHeader": "発生", "anomalyLabel": "異常", "patternDetailsTitle": "パターン詳細", - "patternXofY": "{total} のパターン {インデックス}", - "patternXofYShort": "{合計} の {インデックス}", + "patternXofY": "{total} のパターン {index}", + "patternXofYShort": "{total} の {index}", "patternStatistics": "統計情報", "patternOccurrences": "オカレンス", "patternPercentage": "パーセンテージ", @@ -840,12 +840,12 @@ "patternAnomalyRarePlural": "このパターンはまれなパターンとして検出されます (ログの {pct}%、発生回数は {freq})", "patternAnomalyLowFreq": "このパターンは予想よりも少ない頻度で現れます ({freq} vs avg {avg}、Z スコア:{z})", "patternAnomalyLowFreqPlural": "このパターンは予想よりも少ない頻度で現れます ({freq} vs avg {avg}、Z スコア:{z})", - "patternZScore": "Z スコア:{スコア} (平均周波数:{平均周波数})", + "patternZScore": "Z スコア:{zScore} (平均周波数:{avgFrequency})", "patternVariablesHeader": "[変数]", "patternVariablesDetected": "{count} 変数が検出されました | {count} 変数が検出されました", "patternNoVariablesDetected": "変数が検出されませんでした", "patternTemplate": "パターンテンプレート", - "patternVariablesWithCount": "変数 ({カウント})", + "patternVariablesWithCount": "変数 ({count})", "patternVariableNameColumn": "変数名", "patternVariableTypeColumn": "タイプ", "patternExampleLogsWithCount": "サンプルログ ({count})", @@ -1021,7 +1021,7 @@ "editor": "エディター", "addToDashboard": "ダッシュボードに追加", "selectStream": "[ストリーム] を選択", - "pattern_summary": "{totalTime} ミリ秒で {logsAnalyzed} イベントで見つかった {totalEvents} イベントと {PatternsFound} パターンのうち 1 ~ 50 件を表示しています。", + "pattern_summary": "{totalTime} ミリ秒で {logsAnalyzed} イベントで見つかった {totalEvents} イベントと {patternsFound} パターンのうち 1 ~ 50 件を表示しています。", "histogramTitle": "{start} から {end} まで、{total} 件のイベント中 {took} ms で表示しています。", "histogramTitleWithScan": "{start} から {end} まで、{total} 件のイベント中 {took} ms で表示しています。({scanLabel}: {scanSize})", "recordsChip": "{start} から {end} まで({total} イベント中)", @@ -1115,20 +1115,20 @@ "trendFlat": "フラット", "trendNoData": "選択した時間範囲に発生はありません", "eventsUnit": "イベント", - "firstSeenAgo": "最初の {時間}", + "firstSeenAgo": "最初の {time}", "errorsOverTime": "時間の経過に伴うエラー", "noErrorsInWindow": "選択した時間範囲にエラーはありません", "unhandled": "未処理", "handled": "処理済み", - "deployMarker": "▲ {バージョン} デプロイ", + "deployMarker": "▲ {version} デプロイ", "spikeAfterDeploy": "{version} デプロイ直後の {factor} × スパイク", "errorKpisAria": "エラーメトリクスの概要", "totalErrors": "エラー総数", "acrossUniqueIssues": "{count} 件のユニークな課題全体", "crashFreeSessions": "クラッシュフリーセッション", - "sessionsHitError": "{TotalSessions} セッションの {ErrorSessions} にエラーが発生しました", + "sessionsHitError": "{totalSessions} セッションの {errorSessions} にエラーが発生しました", "usersAffected": "影響を受けるユーザー", - "ofActiveUsers": "{合計} 人のアクティブユーザー数·{pct}%", + "ofActiveUsers": "{total} 人のアクティブユーザー数·{pct}%", "newIssues": "新しい問題", "firstSeenSinceDeploy": "{version} のデプロイ以来初めて見られた", "firstSeenInWindow": "選択したウィンドウで最初に表示される", @@ -1199,7 +1199,7 @@ "inTimeRange": "選択した時間範囲内", "sessionsWithErrors": "エラーが 1 以上のセッション", "rageDeadClicks": "レイジ&デッドクリック", - "ofTotal": "{合計} の {カウント}", + "ofTotal": "{total} の {count}", "noMatchingSessions": "この時間範囲では、選択したフィルターに一致するセッションはありません。", "resetFilters": "フィルターをリセット", "errorCountTooltip": "このセッションの {count} エラー", @@ -1219,8 +1219,8 @@ "insightErrorSpike": "エラーのあるセッションは {rate} × 前の期間(このウィンドウでは {count})です。", "activity": "アクティビティ", "noActivityBounced": "アクティビティなし — すぐにバウンスされます", - "eventsCount": "{カウント} イベント", - "activityAriaLabel": "セッション時間中の {イベント} イベント、{エラー} エラー、{フラストレーション} フラストレーション", + "eventsCount": "{count} イベント", + "activityAriaLabel": "セッション時間中の {events} イベント、{errors} エラー、{frustrations} フラストレーション", "timestamp": "タイムスタンプ", "type": "タイプ", "viewURL": "ビューURL", @@ -1724,7 +1724,7 @@ "confirmRejectMsg": "{org} への招待を拒否してもよろしいですか?", "expired": "期限切れ", "expiresToday": "本日期限切れ", - "daysLeft": "残り {日} 日" + "daysLeft": "残り {days} 日" }, "serviceAccounts": { "header": "サービスアカウント", @@ -2132,7 +2132,7 @@ "orgBeingDeletedSwitching": "この組織は削除処理中です。別の組織に切り替えます。", "createOrganization": "組織を作成", "updateOrganization": "組織を更新", - "makeBilledMember": "この組織を {org} の請求メンバーにする", + "makeBilledMember": "この組織を現在の組織の請求メンバーとして追加します。", "add": "組織を追加", "save": "保存", "cancel": "キャンセル", @@ -2859,7 +2859,7 @@ "benefit2": "他のユーザーからのヘルプ、ダッシュボード、クエリのヒントを設定する", "benefit3": "リリースに関する初期の言葉", "communityNote": "日々活動しているエンジニアとOpenObserveチーム", - "membersCount": "{カウント} 人のメンバー", + "membersCount": "{count} 人のメンバー", "joinSlack": "Slack に参加しよう", "maybeLater": "たぶん後で" }, @@ -3690,7 +3690,7 @@ "showLegendsLabel": "レジェンドを表示", "showAllLegends": "すべてのレジェンドを表示", "legendsOfCharts": "レジェンド・オブ・チャート", - "totalLegends": "合計:{カウント}", + "totalLegends": "合計:{count}", "noLegendsAvailable": "レジェンドはありません", "tableFiltering": "列フィルタリング", "columnFormattingTitle": "列のフォーマット", @@ -3784,7 +3784,7 @@ "maximum": "最大", "fixedValue": "固定値", "legendLabel": "レジェンド", - "overrideMessage": "シリーズ名のオーバーライド。たとえば、{endpoint} はエンドポイントのラベル値に置き換える必要があります。", + "overrideMessage": "シリーズ名のオーバーライド。", "overrideMessageExample": "たとえば、{endpoint} はエンドポイントのラベル値に置き換えられます。", "unsavedMessage": "保存されていない変更があります。本当に終了してもよろしいですか?", "addFieldMessage": "リストからフィールドを追加してください", @@ -4834,7 +4834,7 @@ "recentEvents": "最近のイベント", "investigate": "調査する →", "viewAll": "すべて表示", - "showingOf": "{total} の {表示済み} を表示中", + "showingOf": "{total} の {shown} を表示中", "colErrorRate": "エラー率", "colLatency": "レイテンシー", "colReqs": "必要条件", @@ -5089,7 +5089,7 @@ "inviteAccepted": "招待は正常に受理されました", "inviteRejected": "招待は正常に拒否されました", "plansManagedTitle": "親組織による請求管理", - "plansManagedDescription": "この組織の請求は {payer} によって管理されています。プランの変更は親組織によって処理されます。", + "plansManagedDescription": "この組織の請求は {name} ({id}) によって管理されています。プランの変更は親組織によって処理されます。", "chipPlanManaged": "親が管理するプラン", "chipNoInvoices": "個別の請求書は不要", "viewOrgGroup": "組織グループを表示", @@ -5884,7 +5884,7 @@ "currentWindow": "現在のウィンドウ", "cycle": "サイクル", "cycleTooltip": "結果を前のサイクルの同じ時間と比較します。", - "runningFor": "毎回 {頻度} の間隔で {period} 実行中", + "runningFor": "毎回 {frequency} の間隔で {period} 実行中", "currentWindowCannotBeDeleted": "現在のウィンドウは削除できません", "comparingWith": "との比較", "referenceWindow": "リファレンスウィンドウ", @@ -5939,7 +5939,7 @@ "helpBuiltInFooter": "ストリームのフィールドは変数としても機能します。詳細設定:{'{'} 行:N {'}'} は表示する行数を制限します。{'{'} Var: N {'}'} は値を短縮します。", "helpYourVariablesHeading": "追加した変数", "helpYourVariablesEmpty": "変数はまだ追加されていません。", - "helpRowTemplateExplain": "アラートは複数の行と一致する可能性があります。行テンプレートは 1 つの行の表示方法を決定します。次に、メインテンプレートの {'{'} rows {'} rows {'} '} は、既にフォーマットされているマッチしたすべての行にドロップされます。", + "helpRowTemplateExplain": "アラートは複数の行と一致する可能性があります。行テンプレートは 1 つの行の表示方法を決定します。次に、メインテンプレートの {'{'} rows {'}'} は、既にフォーマットされているマッチしたすべての行にドロップされます。", "helpRowTemplateTypeHeading": "文字列とJSON", "helpRowTemplateTypeExplain": "String は行ごとにプレーンテキストを生成します。JSON は行ごとに JSON オブジェクトを生成するので、構造化された宛先に適しています。", "helpRowTemplateTypeCurrent": "現在選択されているもの:", @@ -5951,10 +5951,10 @@ "helpWhyWithoutCode": "「text」:「重要度:高 — 支払いによる負担」", "helpWhyWithLabel": "変数付き", "helpWhyWithDesc": "1 つの共有テンプレートが変数を参照します。各アラートはここに独自の値を設定します。", - "helpWhyWithCode": "「text」:「重要度:{'{'} 重要度 {'}'} — {'} チーム {'} '} 所有」", + "helpWhyWithCode": "「text」:「重要度:{'{'} 重要度 {'}'} — {'{'} チーム {'}'} 所有」", "helpExampleHeading": "例", "helpVariablesExampleCaption": "重要度とチームを変数として追加し、テンプレート内の任意の場所で使用できます。", - "helpVariablesExampleCode": "{'{'}\n 「text」:「[{'{'} 重要度 {'}'}] {'} alert_name {'} alert_name {'}'} — {'} alert_count {'} '} イベント\\n {'} team {'}'} が所有」\n{'}'}", + "helpVariablesExampleCode": "{'{'}\n 「text」:「[{'{'} 重要度 {'}'}] {'{'} alert_name {'}'} — {'{'} alert_count {'}'} イベント\\n{'{'} team {'}'} が所有」\n{'}'}", "helpExampleResultLabel": "送信される内容", "helpVariablesExampleResult": "{'{'}「text」:「[高] CPU 使用率が高い — 42 イベント\\n 支払いによる所有」{'}'}", "helpTemplateWhenHeading": "いつオーバーライドすればいいですか?", @@ -5963,7 +5963,7 @@ "helpRowTemplateWhyDesc": "アラートは多数のレコードを照合できます。行テンプレートは 1 つのレコードをフォーマットし、{'{'} rows {'}'} はフォーマットされたすべてのレコードをメインテンプレートにドロップします。そのため、ループを作成せずに一致したレコードをすべて一覧表示できます。", "helpRowTemplateExampleCaption": "一致する各行は行テンプレートによってフォーマットされ、{'{'} rows {'}'} はそれらをすべてメインテンプレートにドロップします。", "helpRowTemplateExampleRowLabel": "行テンプレート (1 レコードをフォーマット)", - "helpRowTemplateExampleRowCode": "{'{'} pod {'}'}: {'{'} レベル {'}'} at {'} _timestamp {'} '}", + "helpRowTemplateExampleRowCode": "{'{'} pod {'}'}: {'{'} レベル {'}'} at {'{'} _timestamp {'}'}", "helpRowTemplateExampleMainLabel": "メインテンプレート ({'{'} 行 {'}'} を使用)", "helpRowTemplateExampleMainCode": "{'{'}「text」:「{'{'} alert_count {'}'} 一致:\\n {'{'} 行 {'}'}」{'}'}", "helpRowTemplateExampleResult": "{'{'}「text」:「2 matchs:\\nweb -1:14:02 にエラー\\n web-3:14:05 にエラー」{'}'}", @@ -7167,10 +7167,10 @@ "allUsersBlockedMessage": "メールアドレスが {domain} で終わるすべての外部SSOユーザーは、システム(UI、API、インジェスト)からブロックされます。", "blockedUsersHint": "ブロックされたエントリは、外部 SSO ID (SSO ユーザーと SSO/トークンサービスアカウント) にのみ適用されます。これらは許可ルールよりも優先され、UI、API、インジェストが対象となります。ネイティブアカウントと API で作成されたアカウントは影響を受けません。代わりに削除してください。", "addBlockedEmail": "メールをブロック", - "blockedEmailPlaceholder": "例:ユーザー {ドメイン}", + "blockedEmailPlaceholder": "例:ユーザー {domain}", "domainNeedsBlockedEmails": "特定のユーザーをブロックする場合、ドメイン '{domain}' には少なくとも 1 つのメールアドレスが必要です", "confirmRemoveBlockedEmail": "メール '{email}' のブロックを解除してよろしいですか?", - "emailPlaceholder": "例:ユーザー {ドメイン}", + "emailPlaceholder": "例:ユーザー {domain}", "addEmail": "Eメールを追加", "saveChanges": "[保存]", "invalidDomain": "有効なドメイン名を入力してください", @@ -7903,7 +7903,7 @@ "noEventsPresent": "このスパンにイベントはありません", "noLinksPresent": "このスパンにリンクはありません", "loadingTraceDag": "トレースDAGを読み込み中...", - "exceptionsWithCount": "例外 ({カウント})", + "exceptionsWithCount": "例外 ({count})", "typeLabel": "タイプ:", "messageLabel": "メッセージ:", "escapedLabel": "エスケープ:", @@ -7920,7 +7920,7 @@ "sessions": "セッション", "failedToLoad": "セッションを読み込めませんでした", "retry": "リトライ", - "tokenTooltip": "入力:{入力} · 出力:{出力} · 合計:{合計}", + "tokenTooltip": "入力:{input} · 出力:{output} · 合計:{total}", "durationNs": "ns", "unknownUser": "不明なユーザー", "columns": { @@ -7952,7 +7952,7 @@ "searchPlaceholder": "このセッションのトレースを検索...", "turn": "順番", "turns": "ターン", - "turnCount": "{カウント} {単位}", + "turnCount": "{count} {unit}", "openInTraceExplorer": "トレースを表示", "filters": { "status": "ステータス", @@ -7982,15 +7982,15 @@ }, "kpiSub": { "turns": "コンプリートセッション·{duration}", - "errorsLead": "{合計} の {エラー} · ターン", - "errorsLeadMany": "{合計} ターンの {エラー}", + "errorsLead": "{total} の {errors} · ターン", + "errorsLeadMany": "{total} ターンの {errors}", "filterErrors": "フィルターエラー", "resetFilters": "フィルターをリセット", "errorsNone": "エラーなし·{total} ターン", - "latencyLead": "最も遅い {最も遅い} ·", - "costLead": "ピーク {ピーク} ·", + "latencyLead": "最も遅い {slowest} ·", + "costLead": "ピーク {peak} ·", "tokens": "{ratio}% キャッシュ済み", - "cacheImpact": "{ratio}% 再利用済み·{token} 既読済み", + "cacheImpact": "{ratio}% 再利用済み·{tokens} 既読済み", "cacheEstimate": "正味の影響には、キャッシュ読み取りコストとキャッシュ作成コストが含まれます。", "cacheImpactTooltipTitle": "キャッシュコストの内訳", "actualCost": "実際の費用", @@ -8055,7 +8055,7 @@ "external": "外部", "rpc": "RPC" }, - "unhealthyTooltip": "{カウント} {ステータス}", + "unhealthyTooltip": "{count} {status}", "legend": { "title": "エラー率" } @@ -8676,7 +8676,7 @@ "dimensionBasedCorrelation": "ディメンションベースの相関", "tracesFromService": "サービスからのトレース:{service}", "noTracesFound": "相関トレースは見つかりませんでした", - "noTracesDescription": "選択した時間範囲のサービス {service} のトレースが見つかりません", + "noTracesDescription": "選択した時間範囲でこの相関のトレースが見つかりません。", "viewInTraces": "トレースで表示", "filters": "フィルター", "semanticFieldGroupsCaption": "相関と重複排除のためのフィールド名マッピングを定義します (例:ホスト、ホスト名、ノード →「ホスト」グループ)", @@ -8690,7 +8690,7 @@ "fieldNamePlaceholder": "フィールド名 (カンマ区切り)", "category": "カテゴリー", "categoryHint": "グループをカテゴリで絞り込む", - "showingGroups": "{localGroupLength} 個の合計グループの {フィルターグループの長さ} を表示中", + "showingGroups": "{localGroupLength} 個の合計グループの {filterGroupLength} を表示中", "deduplicateFields": "重複排除フィールド", "deduplicateFieldTooltip": "重複排除フィンガープリントに使用するフィールドマッピングを選択する", "alertDeduplicationMessage": "これらのフィールドの値が同じアラートは重複排除されます (例:同じフィンガープリント = 同じフィールド値)", @@ -8847,7 +8847,7 @@ "testBtn": "テスト", "importBtn": "インポート", "newModel": "新しいモデル", - "modelsCount": "{カウント} モデル", + "modelsCount": "{count} モデル", "exportSelected": "エクスポート ({count})", "deleteSelected": "削除 ({count})", "importTitle": "輸入モデル価格", @@ -9721,7 +9721,7 @@ }, "drillTitle": "ドリル・トゥ・トレース", "trendTitle": "トレンド", - "trendSubtitle": "{設定名}、{バケット}", + "trendSubtitle": "{configName}、{bucket}", "distributionTitle": "スコア分布", "compositionTitle": "時間の経過に伴う構成", "categoryDistributionTitle": "カテゴリ分布", @@ -9731,7 +9731,7 @@ "trueFalseTitle": "真と偽", "legendAvg": "平均", "legendP95": "95", - "legendThreshold": "しきい値 {方向} {値}", + "legendThreshold": "しきい値 {direction} {value}", "legendHealthy": "健康", "legendUnhealthy": "不健全です", "legendHealthyRate": "健全率", @@ -9966,7 +9966,7 @@ }, "active": "有効", "usedByScorers": "{count}個のスコアラー", - "usedByScorer": "{カウント} スコアラー", + "usedByScorer": "{count} スコアラー", "noHealthy": "—", "createInfoNumeric": "このディメンションのスコアが健全とみなされる条件を定義します。この範囲外のスコアは、トレース一覧、セッション、その他のビューで品質の問題をトリガーします。", "editInfoBannerEmphasis": { @@ -10466,7 +10466,7 @@ "trace": "対象となるトレースごとに実行するスコアラーを1つ以上選択します。", "session": "対象となるセッションごとに実行するスコアラーを1つ以上選択します。" }, - "meta": "{タイプ} · v {バージョン}", + "meta": "{type} · v{version}", "placeholder": "スコアラーを選択", "searchPlaceholder": "スコアラーを検索…", "empty": "利用可能なスコアラーがありません。まずスコアラータブで作成してください。" @@ -10507,7 +10507,7 @@ }, "selectScorers": "スコアラーを選択してマッピングを設定します。", "noVariables": "このスコアラーには、マップするテンプレート変数がありません。", - "variableCount": "{カウント} 変数", + "variableCount": "{count} 変数", "systemProvided": { "about": "システム提供の値について", "title": "{scope} システム値", @@ -10950,8 +10950,8 @@ }, "refreshButton": { "justNow": "たった今", - "secondsAgo": "{秒} 秒前", - "minutesAgo": "{最小} 分前", + "secondsAgo": "{sec} 秒前", + "minutesAgo": "{min} 分前", "hoursAgo": "{h} 時間前", "notYetRefreshed": "まだ更新されていません", "dataFresh": "データは新鮮", diff --git a/web/src/locales/languages/ko-KR.json b/web/src/locales/languages/ko-KR.json index 06da7127be..4befa45ee8 100644 --- a/web/src/locales/languages/ko-KR.json +++ b/web/src/locales/languages/ko-KR.json @@ -387,7 +387,7 @@ "description": "보내거나 받은 초대장이 여기에 표시됩니다." }, "filtered": { - "title": "{명사} 를 찾을 수 없습니다.", + "title": "{noun} 를 찾을 수 없습니다.", "description": "현재 필터 또는 검색과 일치하는 {noun} 이 (가) 없습니다.조정하거나 지워 보세요.", "action": "필터 지우기", "actionDesc": "모든 필터 재설정 및 검색", @@ -829,8 +829,8 @@ "occurrenceColumnHeader": "발생", "anomalyLabel": "변칙", "patternDetailsTitle": "패턴 세부 정보", - "patternXofY": "{합계} 의 패턴 {인덱스}", - "patternXofYShort": "{전체} 의 {인덱스}", + "patternXofY": "{total} 의 패턴 {index}", + "patternXofYShort": "{total} 의 {index}", "patternStatistics": "통계학", "patternOccurrences": "발생 횟수", "patternPercentage": "백분율", @@ -840,12 +840,12 @@ "patternAnomalyRarePlural": "이 패턴은 드문 패턴으로 감지됩니다 (로그의 {pct}%, 발생 횟수 {freq} 개)", "patternAnomalyLowFreq": "이 패턴은 예상보다 자주 나타나지 않습니다 ({freq} vs avg {avg}, z-score: {z})", "patternAnomalyLowFreqPlural": "이 패턴은 예상보다 자주 나타나지 않습니다 ({freq} vs avg {avg}, z-score: {z})", - "patternZScore": "Z-점수: {z점수} (평균 주파수: {평균 주파수})", + "patternZScore": "Z-점수: {zScore} (평균 주파수: {avgFrequency})", "patternVariablesHeader": "변수", "patternVariablesDetected": "{count} 변수 감지됨 | {count} 변수 감지됨", "patternNoVariablesDetected": "변수가 감지되지 않음", "patternTemplate": "패턴 템플릿", - "patternVariablesWithCount": "변수 ({개수})", + "patternVariablesWithCount": "변수 ({count})", "patternVariableNameColumn": "변수 이름", "patternVariableTypeColumn": "타입", "patternExampleLogsWithCount": "예제 로그 ({count})", @@ -1021,7 +1021,7 @@ "editor": "에디터", "addToDashboard": "대시보드에 추가", "selectStream": "스트림 선택", - "pattern_summary": "{TotalTime} 밀리초 내에 {logsAnallets} 이벤트에서 발견된 {TotalEvents} 이벤트 및 {PatternsFound} 패턴 중 1~50개를 표시합니다.", + "pattern_summary": "{totalTime} 밀리초 내에 {logsAnalyzed} 이벤트에서 발견된 {totalEvents} 이벤트 및 {patternsFound} 패턴 중 1~50개를 표시합니다.", "histogramTitle": "총 {total}개 이벤트 중 {start}~{end}을(를) {took} ms에 표시 중입니다.", "histogramTitleWithScan": "총 {total}개 이벤트 중 {start}~{end}을(를) {took} ms에 표시 중입니다. ({scanLabel}: {scanSize})", "recordsChip": "총 {total}개 이벤트 중 {start}~{end}", @@ -1115,20 +1115,20 @@ "trendFlat": "플랫", "trendNoData": "선택한 시간 범위 내에 발생 횟수가 없습니다.", "eventsUnit": "행사", - "firstSeenAgo": "처음 {시간}", + "firstSeenAgo": "처음 {time}", "errorsOverTime": "시간 경과에 따른 오류", "noErrorsInWindow": "선택한 시간 범위에 오류가 없습니다.", "unhandled": "처리되지 않음", "handled": "처리됨", - "deployMarker": "▲ {버전} 배포", - "spikeAfterDeploy": "{version} 배포 직후 {팩터} × 급증", + "deployMarker": "▲ {version} 배포", + "spikeAfterDeploy": "{version} 배포 직후 {factor} × 급증", "errorKpisAria": "오류 지표 요약", "totalErrors": "전체 오류", "acrossUniqueIssues": "{count} 개의 고유 문제에 대해", "crashFreeSessions": "크래시 프리 세션", - "sessionsHitError": "{TotalSessions} 세션의 {ErrorSession} 에서 오류가 발생했습니다.", + "sessionsHitError": "{totalSessions} 세션의 {errorSessions} 에서 오류가 발생했습니다.", "usersAffected": "영향을 받은 사용자", - "ofActiveUsers": "활성 사용자 {총} 명 · {pct}%", + "ofActiveUsers": "활성 사용자 {total} 명 · {pct}%", "newIssues": "새 이슈", "firstSeenSinceDeploy": "{version} 배포 이후 처음 확인", "firstSeenInWindow": "선택한 창에 처음 표시됨", @@ -1199,7 +1199,7 @@ "inTimeRange": "선택한 시간 범위 내", "sessionsWithErrors": "오류가 1보다 큰 세션", "rageDeadClicks": "레이지 앤 데드 클릭", - "ofTotal": "{총} 의 {개수}", + "ofTotal": "{total} 의 {count}", "noMatchingSessions": "이 시간 범위에서 선택한 필터와 일치하는 세션이 없습니다.", "resetFilters": "필터 재설정", "errorCountTooltip": "이 세션의 {count} 개 오류", @@ -1219,8 +1219,8 @@ "insightErrorSpike": "오류가 있는 세션은 이전 기간의 {rate} × (이 창에서는 {count}) 입니다.", "activity": "활동", "noActivityBounced": "활동 없음 — 즉시 반송됨", - "eventsCount": "{개수} 개 이벤트", - "activityAriaLabel": "{events} 이벤트, {error} 오류, {좌절} 세션 시간 동안의 좌절", + "eventsCount": "{count} 개 이벤트", + "activityAriaLabel": "세션 시간 동안 {events} 이벤트, {errors} 오류, {frustrations} 좌절", "timestamp": "타임스탬프", "type": "타입", "viewURL": "URL 보기", @@ -1471,7 +1471,7 @@ "hero_title": "나만의 엔터프라이즈 에디션", "offer_text": "모든 기능을 활성화한 상태로 OpenObserve Enterprise를 실행하고 있습니다.", "badge_text_unlimited": "섭취 제한: 무제한", - "badge_text_limited": "섭취 제한: {한도} GB/일", + "badge_text_limited": "섭취 제한: {limit} GB/일", "features_title": "액티브 엔터프라이즈 기능", "features_subtitle": "모든 기능이 잠금 해제되어 바로 사용 가능", "primary_button_text": "라이선스 관리" @@ -1724,7 +1724,7 @@ "confirmRejectMsg": "{org} 가입 초대를 정말로 거부하시겠습니까?", "expired": "만료", "expiresToday": "오늘 만료", - "daysLeft": "{일} 일 남음" + "daysLeft": "{days} 일 남음" }, "serviceAccounts": { "header": "서비스 계정", @@ -2132,7 +2132,7 @@ "orgBeingDeletedSwitching": "이 조직이 삭제 중입니다. 다른 조직으로 전환합니다.", "createOrganization": "조직 생성", "updateOrganization": "조직 업데이트", - "makeBilledMember": "이 조직을 {org} 의 결제 회원으로 설정", + "makeBilledMember": "이 조직을 현재 조직의 결제 회원으로 추가합니다.", "add": "조직 추가", "save": "저장", "cancel": "취소", @@ -2859,7 +2859,7 @@ "benefit2": "다른 사용자의 도움말, 대시보드 및 쿼리 팁 설정", "benefit3": "릴리스에 대한 초기 소문", "communityNote": "매일 활동하는 엔지니어와 OpenObserve 팀", - "membersCount": "{개수} 명의 회원", + "membersCount": "{count} 명의 회원", "joinSlack": "슬랙에 가입하세요", "maybeLater": "아마 나중에" }, @@ -3690,7 +3690,7 @@ "showLegendsLabel": "쇼 레전드", "showAllLegends": "모든 레전드 보기", "legendsOfCharts": "레전드 오브 차트", - "totalLegends": "합계: {개수}", + "totalLegends": "합계: {count}", "noLegendsAvailable": "레전드 없음", "tableFiltering": "컬럼 필터링", "columnFormattingTitle": "열 형식 지정", @@ -3784,7 +3784,7 @@ "maximum": "최대값", "fixedValue": "고정 가치", "legendLabel": "레전드", - "overrideMessage": "시리즈 이름 재정의.예를 들어, {endpoint} 는 엔드포인트의 레이블 값으로 대체되어야 합니다.", + "overrideMessage": "시리즈 이름 재정의.", "overrideMessageExample": "예를 들어, {endpoint} 는 엔드포인트의 레이블 값으로 대체됩니다.", "unsavedMessage": "변경 내용을 저장하지 않았습니다.정말 떠나고 싶니?", "addFieldMessage": "목록에서 필드를 추가하세요.", @@ -4834,7 +4834,7 @@ "recentEvents": "최근 이벤트", "investigate": "조사 →", "viewAll": "모두 보기", - "showingOf": "{total} 의 {표시} 보기", + "showingOf": "{total} 의 {shown} 보기", "colErrorRate": "오류율", "colLatency": "지연 시간", "colReqs": "요구 사항", @@ -5089,7 +5089,7 @@ "inviteAccepted": "초대가 성공적으로 수락됨", "inviteRejected": "초대가 성공적으로 거부됨", "plansManagedTitle": "상위 조직에서 관리하는 청구", - "plansManagedDescription": "이 조직의 대금 청구는 {payer} 가 관리합니다.플랜 변경은 상위 조직에서 처리합니다.", + "plansManagedDescription": "이 조직의 대금 청구는 {name} ({id}) 가 관리합니다. 플랜 변경은 상위 조직에서 처리합니다.", "chipPlanManaged": "부모가 관리하는 플랜", "chipNoInvoices": "별도 청구서 없음", "viewOrgGroup": "조직 그룹 보기", @@ -5542,7 +5542,7 @@ "preview": "시사", "crontitle": "크론 활성화", "lastTriggered": "마지막 트리거 시간", - "importedFromPanel": "대시보드 패널에서 가져온 쿼리: “{PanelTitle}”", + "importedFromPanel": "대시보드 패널에서 가져온 쿼리: “{panelTitle}”", "lastSatisfied": "최종 만족 시간", "owner": "소유자", "title": "타이틀", @@ -5884,7 +5884,7 @@ "currentWindow": "현재 창", "cycle": "사이클", "cycleTooltip": "이전 주기의 동일한 시간과 결과를 비교합니다.", - "runningFor": "모든 {빈도} 간격으로 {기간} 동안 실행", + "runningFor": "모든 {frequency} 간격으로 {period} 동안 실행", "currentWindowCannotBeDeleted": "현재 창을 삭제할 수 없습니다.", "comparingWith": "비교 대상", "referenceWindow": "레퍼런스 윈도우", @@ -5939,7 +5939,7 @@ "helpBuiltInFooter": "스트림의 필드는 변수로도 작동합니다.고급: {'{'} row:N {'}'} 은 표시되는 행 수를 제한하고, {'{'} var:N {'}'} 은 값을 줄입니다.", "helpYourVariablesHeading": "추가한 변수", "helpYourVariablesEmpty": "아직 변수를 추가하지 않았습니다.", - "helpRowTemplateExplain": "알림은 여러 행과 일치할 수 있습니다.행 템플릿은 단일 행의 모양을 결정합니다.그러면 기본 템플릿의 {'{'} rows {'} row {'} 가 이미 서식이 지정된 모든 일치하는 행에 드롭됩니다.", + "helpRowTemplateExplain": "알림은 여러 행과 일치할 수 있습니다.행 템플릿은 단일 행의 모양을 결정합니다.그러면 기본 템플릿의 {'{'} rows {'}'} 가 이미 서식이 지정된 모든 일치하는 행에 드롭됩니다.", "helpRowTemplateTypeHeading": "문자열과 JSON 비교", "helpRowTemplateTypeExplain": "문자열은 행당 일반 텍스트를 생성합니다.JSON은 구조화된 대상에 적합한 행당 JSON 객체를 생성합니다.", "helpRowTemplateTypeCurrent": "현재 선택됨:", @@ -5954,16 +5954,16 @@ "helpWhyWithCode": "“text”: “심각도: {'{'} 심각도 {'}'} — {'{'} 팀 {'}'} 소유”", "helpExampleHeading": "예시", "helpVariablesExampleCaption": "심각도와 팀을 변수로 추가한 다음 템플릿의 어느 곳에서나 사용할 수 있습니다.", - "helpVariablesExampleCode": "{'{'}\n “text”: “[{'{'} 심각도 {'}'}] {'{'} alert_name {'}'}} — {'{'} alert_count {'}'} 이벤트\\n{'{'} 팀 {'}'} 소유\n{'}'}", + "helpVariablesExampleCode": "{'{'}\n “text”: “[{'{'} 심각도 {'}'}] {'{'} alert_name {'}'} — {'{'} alert_count {'}'} 이벤트\\n{'{'} team {'}'} 소유”\n{'}'}", "helpExampleResultLabel": "전송되는 내용", "helpVariablesExampleResult": "{'{'} “text”: “[하이] CPU 사용량이 많음 — 이벤트 42개\\n결제 소유” {'}'}", "helpTemplateWhenHeading": "언제 오버라이드해야 하나요?", "helpTemplateWhenDesc": "비워 두면 각 목적지마다 고유한 템플릿을 사용합니다.각 대상의 템플릿을 수정하지 않고도 이 하나의 알림으로 모든 대상에 동일하고 일관된 메시지를 전송하도록 하려면 오버라이드를 설정하세요.", "helpRowTemplateWhyHeading": "행 템플릿이 필요한 이유", - "helpRowTemplateWhyDesc": "알림은 여러 레코드와 일치할 수 있습니다.행 템플릿은 하나의 레코드를 포맷합니다. {'{'} rows {'} rows {'} 는 포맷된 모든 레코드를 기본 템플릿에 드롭하므로 루프를 작성하지 않고도 일치하는 모든 항목을 나열할 수 있습니다.", + "helpRowTemplateWhyDesc": "알림은 여러 레코드와 일치할 수 있습니다.행 템플릿은 하나의 레코드를 포맷합니다. {'{'} rows {'}'} 는 포맷된 모든 레코드를 기본 템플릿에 드롭하므로 루프를 작성하지 않고도 일치하는 모든 항목을 나열할 수 있습니다.", "helpRowTemplateExampleCaption": "일치하는 각 행은 행 템플릿으로 형식이 지정되고 {'{'} rows {'}'} 는 모든 행을 기본 템플릿에 드롭합니다.", "helpRowTemplateExampleRowLabel": "행 템플릿 (하나의 레코드 포맷 지정)", - "helpRowTemplateExampleRowCode": "{'{'} pod {'}'}: {'{'} 레벨 {'}'} at {'{'} _타임스탬프 {'}", + "helpRowTemplateExampleRowCode": "{'{'} pod {'}'}: {'{'} 레벨 {'}'} at {'{'} _timestamp {'}'}", "helpRowTemplateExampleMainLabel": "기본 템플릿 ({'{'} 행 {'}'} 사용)", "helpRowTemplateExampleMainCode": "{'{'} “text”: “{'{'} alert_count {'}'} 일치:\\n{'{'} 행 {'}'}” {'}'}", "helpRowTemplateExampleResult": "{'{'} “text”: “2 일치s:\\nweb-1:14:02 오류\\nweb-3:14:05 에 오류\" {'}'}", @@ -7167,10 +7167,10 @@ "allUsersBlockedMessage": "이메일 주소가 {domain} 으로 끝나는 모든 외부 SSO 사용자는 시스템에서 차단됩니다 (UI, API 및 통합).", "blockedUsersHint": "차단된 항목은 외부 SSO ID (SSO 사용자 및 SSO/토큰 서비스 계정) 에만 적용됩니다.이는 모든 허용 규칙보다 우선 적용되며 UI, API 및 수집을 포함합니다.네이티브 계정과 API에서 생성한 계정은 영향을 받지 않습니다. 대신 삭제하세요.", "addBlockedEmail": "이메일 차단", - "blockedEmailPlaceholder": "예: 사용자 {도메인}", + "blockedEmailPlaceholder": "예: 사용자 {domain}", "domainNeedsBlockedEmails": "특정 사용자를 차단하려면 '{domain}' 도메인에 이메일 주소가 하나 이상 있어야 합니다.", "confirmRemoveBlockedEmail": "'{email}' 이메일의 차단을 해제하시겠습니까?", - "emailPlaceholder": "예: 사용자 {도메인}", + "emailPlaceholder": "예: 사용자 {domain}", "addEmail": "이메일 추가", "saveChanges": "저장", "invalidDomain": "유효한 도메인 이름을 입력하세요", @@ -7272,7 +7272,7 @@ "clearRegistryDescription": "이렇게 하면 이 조직에 대해 검색된 GenAI 에이전트 레지스트리 행이 모두 삭제됩니다.일치하는 새 트레이스는 수집 중에 레지스트리를 다시 채웁니다.", "clearRegistryConfirm": "레지스트리 지우기", "saved": "GenAI 에이전트 매핑이 성공적으로 저장되었습니다.", - "clearRegistrySuccess": "GenAI 에이전트 레지스트리가 삭제되었습니다.{DeletedCount} 개의 저장된 에이전트와 {BufferCount} 개의 대기 중인 관찰을 삭제했습니다.", + "clearRegistrySuccess": "GenAI 에이전트 레지스트리가 삭제되었습니다. {deletedCount} 개의 저장된 에이전트와 {bufferCount} 개의 대기 중인 관찰을 삭제했습니다.", "loadFailed": "GenAI 에이전트 매핑을 로드하지 못했습니다.", "saveFailed": "GenAI 에이전트 매핑을 저장하지 못했습니다.", "clearRegistryFailed": "GenAI 에이전트 레지스트리를 지우지 못했습니다.", @@ -7401,7 +7401,7 @@ "awsRoleArn": "AWS 역할 ARN" }, "organizationSettings": { - "fieldNameHelp": "영숫자 및 '+=,.@{'@'}-_' 문자만 사용하고 공백은 사용하지 마세요.", + "fieldNameHelp": "영숫자 및 '+=,.{'@'}-_' 문자만 사용하고 공백은 사용하지 마세요.", "settingsUpdated": "조직 설정이 성공적으로 업데이트되었습니다.", "settingsSaveError": "조직 설정 저장 중 오류가 발생했습니다." }, @@ -7903,7 +7903,7 @@ "noEventsPresent": "이 스팬에 대한 이벤트 없음", "noLinksPresent": "이 스팬에 대한 링크 없음", "loadingTraceDag": "트레이스 DAG 로딩 중...", - "exceptionsWithCount": "예외 ({개수})", + "exceptionsWithCount": "예외 ({count})", "typeLabel": "유형:", "messageLabel": "메시지:", "escapedLabel": "탈출:", @@ -7920,7 +7920,7 @@ "sessions": "세션", "failedToLoad": "세션을 로드하지 못했습니다.", "retry": "재시도", - "tokenTooltip": "입력: {입력} · 출력: {출력} · 합계: {합계}", + "tokenTooltip": "입력: {input} · 출력: {output} · 합계: {total}", "durationNs": "ns", "unknownUser": "알 수 없는 사용자", "columns": { @@ -7952,7 +7952,7 @@ "searchPlaceholder": "이 세션에서 추적 검색...", "turn": "회전", "turns": "회전합니다", - "turnCount": "{개수} {단위}", + "turnCount": "{count} {unit}", "openInTraceExplorer": "트레이스 보기", "filters": { "status": "상태", @@ -7981,16 +7981,16 @@ "cacheImpact": "캐시 영향" }, "kpiSub": { - "turns": "세션 완료 · {기간}", - "errorsLead": "{총} · 턴의 {오류}", - "errorsLeadMany": "{총} 턴의 {오류}", + "turns": "세션 완료 · {duration}", + "errorsLead": "{total} 의 {errors} · 턴", + "errorsLeadMany": "{total} 턴의 {errors}", "filterErrors": "필터 오류", "resetFilters": "필터 재설정", - "errorsNone": "오류 없음 · {총} 턴", - "latencyLead": "가장 느린 {가장 느린} ·", - "costLead": "피크 {피크} ·", - "tokens": "{비율}% 캐시됨", - "cacheImpact": "{비율}% 재사용 · {token} 톡 읽기", + "errorsNone": "오류 없음 · {total} 턴", + "latencyLead": "가장 느린 {slowest} ·", + "costLead": "피크 {peak} ·", + "tokens": "{ratio}% 캐시됨", + "cacheImpact": "{ratio}% 재사용 · {tokens} 톡 읽기", "cacheEstimate": "순 영향에는 캐시 읽기 및 캐시 생성 비용이 포함됩니다.", "cacheImpactTooltipTitle": "캐시 비용 분석", "actualCost": "실제 비용", @@ -8055,7 +8055,7 @@ "external": "외부", "rpc": "RPC" }, - "unhealthyTooltip": "{개수} {상태}", + "unhealthyTooltip": "{count} {status}", "legend": { "title": "오류율" } @@ -8655,7 +8655,7 @@ "noData": "상관관계가 있는 로그를 찾을 수 없음", "noDataDetails": "상관관계 필터와 일치하는 로그를 찾을 수 없습니다.", "unstableDimension": "불안정한 치수 - 포드 재시작 시 변경될 수 있음", - "resultsCount": "{stream} ({time} ms) 에서 찾은 로그 {개수} 개", + "resultsCount": "{stream} ({time} ms) 에서 찾은 로그 {count} 개", "timeRange": { "startBeforeEnd": "시작 시간은 종료 시간 이전이어야 합니다.", "endAfterStart": "종료 시간은 시작 시간 이후여야 합니다." @@ -8676,7 +8676,7 @@ "dimensionBasedCorrelation": "차원 기반 상관관계", "tracesFromService": "서비스의 추적: {service}", "noTracesFound": "상관관계가 있는 흔적을 찾을 수 없습니다.", - "noTracesDescription": "선택한 시간 범위에서 서비스 {service} 에 대한 추적을 찾을 수 없습니다.", + "noTracesDescription": "선택한 시간 범위에서 이 상관관계에 대한 추적을 찾을 수 없습니다.", "viewInTraces": "트레이스에서 보기", "filters": "필터", "semanticFieldGroupsCaption": "상관 관계 분석 및 중복 제거를 위한 필드 이름 매핑 정의 (예: 호스트, 호스트 이름, 노드 → 'host' 그룹)", @@ -8690,7 +8690,7 @@ "fieldNamePlaceholder": "필드 이름 (쉼표로 구분)", "category": "카테고리", "categoryHint": "범주별 그룹 필터링", - "showingGroups": "{로컬 그룹 길이} 전체 그룹의 {필터 그룹 길이} 표시", + "showingGroups": "{localGroupLength} 전체 그룹의 {filterGroupLength} 표시", "deduplicateFields": "중복 제거 필드", "deduplicateFieldTooltip": "중복 제거 핑거프린팅에 사용할 필드 매핑 선택", "alertDeduplicationMessage": "이러한 필드에 동일한 값을 가진 알림은 중복 제거됩니다 (예: 동일한 핑거프린트 = 동일한 필드 값).", @@ -8811,7 +8811,7 @@ "fields": "필드", "fieldsHint": "레코드에 하나 이상의 필드가 있는 경우에만 링크 표시", "fieldInputPlaceholder": "필드 이름을 입력하고 Enter 키를 누르거나 +를 클릭합니다.", - "emptyState": "크로스링크가 구성되지 않았습니다.“{AddLabel}”을 클릭하여 생성하십시오.", + "emptyState": "크로스링크가 구성되지 않았습니다.“{addLabel}”을 클릭하여 생성하십시오.", "update": "업데이트", "add": "추가", "streamCrossLinks": "스트림 크로스링크", @@ -8847,9 +8847,9 @@ "testBtn": "테스트", "importBtn": "임포트", "newModel": "새 모델", - "modelsCount": "{개수} 모델", - "exportSelected": "내보내기 ({개수})", - "deleteSelected": "삭제 ({개수})", + "modelsCount": "{count} 모델", + "exportSelected": "내보내기 ({count})", + "deleteSelected": "삭제 ({count})", "importTitle": "수입 모델 가격", "errorValidations": "오류 검증", "outputMessages": "출력 메시지", @@ -8904,7 +8904,7 @@ "enterModelName": "매칭을 테스트할 모델 이름을 입력합니다.", "clickToTest": "결과를 보려면 “테스트 매치”를 클릭하십시오.", "noMatchFound": "일치하는 항목 없음", - "noMatchDesc": "“{모델이름}”과 일치하는 규칙이 없습니다.", + "noMatchDesc": "“{modelName}”과 일치하는 규칙이 없습니다.", "troubleshootingTitle": "문제 해결 팁:", "tip1": "모델 이름의 철자를 확인하세요", "tip2": "일치하는 정규식 패턴을 가진 규칙이 존재하고 활성화되었는지 확인합니다.", @@ -9721,7 +9721,7 @@ }, "drillTitle": "드릴 투 트레이스", "trendTitle": "트렌드", - "trendSubtitle": "{구성 이름}, {버킷}", + "trendSubtitle": "{configName}, {bucket}", "distributionTitle": "점수 분포", "compositionTitle": "시간 경과에 따른 구성", "categoryDistributionTitle": "카테고리 분포", @@ -9731,7 +9731,7 @@ "trueFalseTitle": "진실 대 거짓", "legendAvg": "평균", "legendP95": "p95", - "legendThreshold": "임계값 {방향} {값}", + "legendThreshold": "임계값 {direction} {value}", "legendHealthy": "건강한", "legendUnhealthy": "건강에 해로운", "legendHealthyRate": "건강 비율", @@ -9966,7 +9966,7 @@ }, "active": "활성", "usedByScorers": "스코어러 {count}개", - "usedByScorer": "{개수} 득점자", + "usedByScorer": "{count} 득점자", "noHealthy": "—", "createInfoNumeric": "이 차원의 점수가 정상으로 간주되는 시점을 정의하세요. 이 범위를 벗어나는 점수는 트레이스 목록, 세션 및 기타 보기에서 품질 문제를 트리거합니다.", "editInfoBannerEmphasis": { @@ -10466,7 +10466,7 @@ "trace": "적격한 각 트레이스에 대해 실행할 하나 이상의 스코어러를 선택합니다.", "session": "적격한 각 세션에 대해 실행할 하나 이상의 스코어러를 선택합니다." }, - "meta": "{유형} · v {버전}", + "meta": "{type} · v{version}", "placeholder": "스코어러 선택", "searchPlaceholder": "스코어러 검색…", "empty": "사용 가능한 스코어러가 없습니다. 먼저 스코어러 탭에서 하나를 생성하세요." @@ -10507,7 +10507,7 @@ }, "selectScorers": "매핑을 구성할 채점자를 선택합니다.", "noVariables": "이 채점기에는 매핑할 템플릿 변수가 없습니다.", - "variableCount": "{개수} 개 변수", + "variableCount": "{count} 개 변수", "systemProvided": { "about": "시스템 제공 값 정보", "title": "{scope} 시스템 값", @@ -10950,8 +10950,8 @@ }, "refreshButton": { "justNow": "바로 지금", - "secondsAgo": "{초} 초 전", - "minutesAgo": "{분} m 전", + "secondsAgo": "{sec} 초 전", + "minutesAgo": "{min} m 전", "hoursAgo": "{h} h 전", "notYetRefreshed": "아직 새로고침되지 않음", "dataFresh": "최신 데이터", diff --git a/web/src/locales/languages/nl-NL.json b/web/src/locales/languages/nl-NL.json index 90ae105d86..fe364d86dc 100644 --- a/web/src/locales/languages/nl-NL.json +++ b/web/src/locales/languages/nl-NL.json @@ -387,7 +387,7 @@ "description": "Uitnodigingen die je hebt verzonden of ontvangen, worden hier weergegeven." }, "filtered": { - "title": "Geen {zelfstandig naamwoord} gevonden", + "title": "Geen {noun} gevonden", "description": "Er komt geen {noun} overeen met je huidige filters of zoekopdracht. Probeer ze aan te passen of te wissen.", "action": "Filters wissen", "actionDesc": "Alle filters opnieuw instellen en zoeken", @@ -1021,7 +1021,7 @@ "editor": "Redacteur", "addToDashboard": "Aan dashboard toevoegen", "selectStream": "Selecteer Stream", - "pattern_summary": "Toont 1 tot 50 van de {totalEvents} -gebeurtenissen en {PatternsFound} -patronen gevonden in {logsAnalyzed} -gebeurtenissen in {totalTime} ms.", + "pattern_summary": "Toont 1 tot 50 van de {totalEvents} -gebeurtenissen en {patternsFound} -patronen gevonden in {logsAnalyzed} -gebeurtenissen in {totalTime} ms.", "histogramTitle": "Toont {start} tot {end} van {total} gebeurtenissen in {took} ms.", "histogramTitleWithScan": "Toont {start} tot {end} van {total} gebeurtenissen in {took} ms. ({scanLabel}: {scanSize})", "recordsChip": "{start} tot {end} van {total} gebeurtenissen", @@ -1115,7 +1115,7 @@ "trendFlat": "vlak", "trendNoData": "Geen voorvallen in het geselecteerde tijdbereik", "eventsUnit": "gebeurtenissen", - "firstSeenAgo": "eerste {keer}", + "firstSeenAgo": "eerste {time}", "errorsOverTime": "Fouten in de loop van de tijd", "noErrorsInWindow": "Geen fouten in het geselecteerde tijdbereik", "unhandled": "Onbehandeld", @@ -1126,9 +1126,9 @@ "totalErrors": "Totaal aantal fouten", "acrossUniqueIssues": "in {count} unieke problemen", "crashFreeSessions": "Sessies zonder crashes", - "sessionsHitError": "{ErrorSessions} van {TotalSessions} sessies hebben een foutmelding", + "sessionsHitError": "{errorSessions} van {totalSessions} sessies hebben een foutmelding", "usersAffected": "Betrokken gebruikers", - "ofActiveUsers": "van {totaal} actieve gebruikers · {pct}%", + "ofActiveUsers": "van {total} actieve gebruikers · {pct}%", "newIssues": "Nieuwe nummers", "firstSeenSinceDeploy": "voor het eerst gezien sinds de implementatie van {version}", "firstSeenInWindow": "voor het eerst gezien in het geselecteerde venster", @@ -1220,7 +1220,7 @@ "activity": "Activiteit", "noActivityBounced": "Geen activiteit — onmiddellijk teruggestuurd", "eventsCount": "{count} evenementen", - "activityAriaLabel": "{events} events, {errors} errors, {frustraties} frustraties tijdens de sessie", + "activityAriaLabel": "{events} events, {errors} errors, {frustrations} frustraties tijdens de sessie", "timestamp": "Tijdstempel", "type": "Typ", "viewURL": "URL bekijken", @@ -1471,7 +1471,7 @@ "hero_title": "Uw Enterprise-editie", "offer_text": "U gebruikt OpenObserve Enterprise met alle functies ingeschakeld", "badge_text_unlimited": "Inslimiet: onbeperkt", - "badge_text_limited": "Inslimiet: {limiet} GB/dag", + "badge_text_limited": "Inslimiet: {limit} GB/dag", "features_title": "Active Enterprise-functies", "features_subtitle": "Alle functies ontgrendeld en klaar voor gebruik", "primary_button_text": "Licentie beheren" @@ -1724,7 +1724,7 @@ "confirmRejectMsg": "Weet je zeker dat je de uitnodiging om lid te worden {org} wilt weigeren?", "expired": "Vervallen", "expiresToday": "Vervalt vandaag", - "daysLeft": "{dagen} resterende dagen" + "daysLeft": "{days} resterende dagen" }, "serviceAccounts": { "header": "Serviceaccounts", @@ -2132,7 +2132,7 @@ "orgBeingDeletedSwitching": "Deze organisatie wordt verwijderd. U wordt omgeschakeld naar een andere organisatie.", "createOrganization": "Organisatie creëren", "updateOrganization": "Organisatie bijwerken", - "makeBilledMember": "Maak van deze organisatie een factureringslid van {org}", + "makeBilledMember": "Voeg deze organisatie toe als factureringslid van de huidige organisatie.", "add": "Organisatie toevoegen", "save": "Opslaan", "cancel": "Annuleer", @@ -3784,7 +3784,7 @@ "maximum": "Maximaal", "fixedValue": "Vaste waarde", "legendLabel": "Legenda", - "overrideMessage": "Overschrijvingen van de serienaam. {endpoint} moet bijvoorbeeld worden vervangen door de labelwaarde voor het eindpunt.", + "overrideMessage": "Overschrijvingen van de serienaam.", "overrideMessageExample": "{endpoint} wordt bijvoorbeeld vervangen door de labelwaarde voor eindpunt.", "unsavedMessage": "Je hebt de wijzigingen niet opgeslagen. Weet je zeker dat je weg wilt?", "addFieldMessage": "Voeg een veld uit de lijst toe", @@ -4834,7 +4834,7 @@ "recentEvents": "Recente evenementen", "investigate": "Onderzoeken →", "viewAll": "Alles bekijken", - "showingOf": "Toont {getoond} van {total}", + "showingOf": "Toont {shown} van {total}", "colErrorRate": "Foutpercentage", "colLatency": "Latentie", "colReqs": "Req's", @@ -5089,7 +5089,7 @@ "inviteAccepted": "Uitnodiging succesvol geaccepteerd", "inviteRejected": "Uitnodiging succesvol afgewezen", "plansManagedTitle": "Facturering beheerd door de moederorganisatie", - "plansManagedDescription": "De facturering van deze organisatie wordt beheerd door {payer}. Wijzigingen in het plan worden afgehandeld door de moederorganisatie.", + "plansManagedDescription": "De facturering van deze organisatie wordt beheerd door {name} ({id}). Wijzigingen in het plan worden afgehandeld door de moederorganisatie.", "chipPlanManaged": "Plan beheerd door de ouder", "chipNoInvoices": "Geen afzonderlijke facturen", "viewOrgGroup": "Organisatiegroep bekijken", @@ -5884,7 +5884,7 @@ "currentWindow": "Huidig venster", "cycle": "Cyclus", "cycleTooltip": "Vergelijk de resultaten met dezelfde periode in de vorige cyclus.", - "runningFor": "Hardlopen gedurende {periode} in het interval van elke {frequentie}", + "runningFor": "Hardlopen gedurende {period} in het interval van elke {frequency}", "currentWindowCannotBeDeleted": "Het huidige venster kan niet worden verwijderd", "comparingWith": "Vergelijken met", "referenceWindow": "Referentievenster", @@ -7952,7 +7952,7 @@ "searchPlaceholder": "Zoek naar sporen in deze sessie...", "turn": "beurt", "turns": "omdraaien", - "turnCount": "{count} {eenheid}", + "turnCount": "{count} {unit}", "openInTraceExplorer": "Trace bekijken", "filters": { "status": "Status", @@ -7987,8 +7987,8 @@ "filterErrors": "Filterfouten", "resetFilters": "Filters opnieuw instellen", "errorsNone": "Geen fouten · {total} Turns", - "latencyLead": "Langzaamste {langzaamste} ·", - "costLead": "Piek {piek} ·", + "latencyLead": "Langzaamste {slowest} ·", + "costLead": "Piek {peak} ·", "tokens": "{ratio}% in cache opgeslagen", "cacheImpact": "{ratio}% Hergebruikt · {tokens} Tok Read", "cacheEstimate": "De netto-impact omvat het lezen van de cache en de aanmaakkosten van de cache.", @@ -8055,7 +8055,7 @@ "external": "Extern", "rpc": "RPC" }, - "unhealthyTooltip": "{aantal} {status}", + "unhealthyTooltip": "{count} {status}", "legend": { "title": "Foutpercentage" } @@ -8676,7 +8676,7 @@ "dimensionBasedCorrelation": "Correlatie op basis van dimensies", "tracesFromService": "Sporen van service: {service}", "noTracesFound": "Geen gecorreleerde sporen gevonden", - "noTracesDescription": "Geen sporen gevonden voor service {service} in de geselecteerde periode", + "noTracesDescription": "Geen traces gevonden voor deze correlatie in de geselecteerde periode.", "viewInTraces": "Bekijk in Traces", "filters": "Filters", "semanticFieldGroupsCaption": "Definieer veldnaamtoewijzingen voor correlatie en deduplicatie (bijv. host, hostnaam, knooppunt → 'host'-groep)", @@ -9731,7 +9731,7 @@ "trueFalseTitle": "Waar versus onwaar", "legendAvg": "Gem", "legendP95": "p95", - "legendThreshold": "drempelwaarde {richting} {waarde}", + "legendThreshold": "drempelwaarde {direction} {value}", "legendHealthy": "gezond", "legendUnhealthy": "ongezond", "legendHealthyRate": "Gezondheidspercentage", @@ -10466,7 +10466,7 @@ "trace": "Selecteer een of meerdere scorers die voor elke in aanmerking komende trace moeten worden uitgevoerd.", "session": "Selecteer een of meerdere scorers die voor elke in aanmerking komende sessie moeten worden uitgevoerd." }, - "meta": "{type} · v {versie}", + "meta": "{type} · v {version}", "placeholder": "Scorers selecteren", "searchPlaceholder": "Scorers zoeken…", "empty": "Geen scorers beschikbaar. Maak er eerst een aan op het tabblad Scorers." diff --git a/web/src/locales/languages/pt-PT.json b/web/src/locales/languages/pt-PT.json index d2701ec625..dc44501045 100644 --- a/web/src/locales/languages/pt-PT.json +++ b/web/src/locales/languages/pt-PT.json @@ -387,8 +387,8 @@ "description": "Os convites que você enviou ou recebeu aparecerão aqui." }, "filtered": { - "title": "Nenhum {substantivo} encontrado", - "description": "Nenhum {substantivo} corresponde aos seus filtros ou pesquisas atuais. Tente ajustá-los ou limpá-los.", + "title": "Nenhum {noun} encontrado", + "description": "Nenhum {noun} corresponde aos seus filtros ou pesquisas atuais. Tente ajustá-los ou limpá-los.", "action": "Filtros claros", "actionDesc": "Redefina todos os filtros e pesquise", "fallbackNoun": "resultados" @@ -571,7 +571,7 @@ "attributes": "Atributos", "events": "Eventos", "goHome": "Vá para casa", - "redirectingHome": "Redirecionando para a página inicial em {contagem regressiva} segundos...", + "redirectingHome": "Redirecionando para a página inicial em {countdown} segundos...", "name": "Nome", "add": "Adicionar", "selectAll": "Selecionar tudo", @@ -829,8 +829,8 @@ "occurrenceColumnHeader": "Ocorrência", "anomalyLabel": "ANOMALIA", "patternDetailsTitle": "Detalhes do padrão", - "patternXofY": "Padrão {índice} de {total}", - "patternXofYShort": "{índice} de {total}", + "patternXofY": "Padrão {index} de {total}", + "patternXofYShort": "{index} de {total}", "patternStatistics": "Estatísticas", "patternOccurrences": "Ocorrências", "patternPercentage": "Porcentagem", @@ -1115,18 +1115,18 @@ "trendFlat": "apartamento", "trendNoData": "Nenhuma ocorrência no intervalo de tempo selecionado", "eventsUnit": "eventos", - "firstSeenAgo": "primeira {vez}", + "firstSeenAgo": "primeira {time}", "errorsOverTime": "Erros ao longo do tempo", "noErrorsInWindow": "Sem erros no intervalo de tempo selecionado", "unhandled": "Não manuseado", "handled": "Manipulado", - "deployMarker": "▲ {versão} implantar", - "spikeAfterDeploy": "{factor} × pico logo após a implantação da {versão}", + "deployMarker": "▲ {version} implantar", + "spikeAfterDeploy": "{factor} × pico logo após a implantação da {version}", "errorKpisAria": "Resumo das métricas de erro", "totalErrors": "Total de erros", "acrossUniqueIssues": "em {count} edições exclusivas", "crashFreeSessions": "Sessões sem falhas", - "sessionsHitError": "{ErrorSessions} das sessões {totalSessions} encontraram um erro", + "sessionsHitError": "{errorSessions} das sessões {totalSessions} encontraram um erro", "usersAffected": "Usuários afetados", "ofActiveUsers": "de {total} usuários ativos · {pct}%", "newIssues": "Novos problemas", @@ -1199,7 +1199,7 @@ "inTimeRange": "no intervalo de tempo selecionado", "sessionsWithErrors": "sessões com erro ≥1", "rageDeadClicks": "raiva e cliques mortos", - "ofTotal": "{contagem} de {total}", + "ofTotal": "{count} de {total}", "noMatchingSessions": "Nenhuma sessão corresponde aos filtros selecionados nesse intervalo de tempo.", "resetFilters": "Redefinir filtros", "errorCountTooltip": "{count} erros nesta sessão", @@ -1220,7 +1220,7 @@ "activity": "Atividade", "noActivityBounced": "Sem atividade — saltou imediatamente", "eventsCount": "{count} eventos", - "activityAriaLabel": "{eventos} eventos, {erros} erros, {frustrações} frustrações sobre o tempo da sessão", + "activityAriaLabel": "{events} eventos, {errors} erros, {frustrations} frustrações sobre o tempo da sessão", "timestamp": "Carimbo de data e hora", "type": "Tipo", "viewURL": "Exibir URL", @@ -1429,7 +1429,7 @@ "usage_information": "Informações de uso", "license_allows_ingestion": "Sua licença permite {limit} GB de ingestão de dados por dia.", "license_allows_unlimited_ingestion": "A licença atual permite a ingestão ilimitada de dados.", - "limit_exceeded_days": "Limite excedido em {dias} dias deste mês", + "limit_exceeded_days": "Limite excedido em {days} dias deste mês", "limit_exceeded_warning": " (máximo de {max} dias permitidos). O recurso do aplicativo será restrito", "limit_exceeded_info": " ({remaining} subsídio{plural} à esquerda de {max} no máximo)", "no_limit_exceedances": "Nenhum limite excede este mês (máximo de {max} permitidos por mês).", @@ -1724,7 +1724,7 @@ "confirmRejectMsg": "Tem certeza de que deseja rejeitar o convite para entrar em {org}?", "expired": "Expirado", "expiresToday": "Expira hoje", - "daysLeft": "{dias} dias restantes" + "daysLeft": "{days} dias restantes" }, "serviceAccounts": { "header": "Contas de serviço", @@ -2132,7 +2132,7 @@ "orgBeingDeletedSwitching": "Esta organização está sendo excluída. Redirecionando você para outra organização.", "createOrganization": "Criar organização", "updateOrganization": "Atualizar organização", - "makeBilledMember": "Torne esta organização um membro de cobrança de {org}", + "makeBilledMember": "Adicionar esta organização como membro de cobrança da organização atual.", "add": "Adicionar organização", "save": "Salvar", "cancel": "Cancelar", @@ -3784,7 +3784,7 @@ "maximum": "Máximo", "fixedValue": "Valor fixo", "legendLabel": "Legenda", - "overrideMessage": "Substituições de nomes de séries. Por exemplo, {endpoint} deve ser substituído pelo valor do rótulo do endpoint.", + "overrideMessage": "Substituições de nomes de séries.", "overrideMessageExample": "Por exemplo, {endpoint} será substituído pelo valor do rótulo para endpoint.", "unsavedMessage": "Você tem alterações não salvas. Tem certeza de que quer sair?", "addFieldMessage": "Adicione um campo da lista", @@ -4834,7 +4834,7 @@ "recentEvents": "Eventos recentes", "investigate": "Investigue →", "viewAll": "Exibir tudo", - "showingOf": "Mostrando {mostrado} de {total}", + "showingOf": "Mostrando {shown} de {total}", "colErrorRate": "Taxa de erro", "colLatency": "Latência", "colReqs": "Requestões", @@ -5089,7 +5089,7 @@ "inviteAccepted": "Convite aceito com sucesso", "inviteRejected": "Convite rejeitado com sucesso", "plansManagedTitle": "Faturamento gerenciado pela organização-mãe", - "plansManagedDescription": "O faturamento dessa organização é gerenciado por {payer}. As mudanças no plano são tratadas pela organização-mãe.", + "plansManagedDescription": "O faturamento dessa organização é gerenciado por {name} ({id}). As mudanças no plano são tratadas pela organização-mãe.", "chipPlanManaged": "Plano gerenciado pelos pais", "chipNoInvoices": "Sem faturas separadas", "viewOrgGroup": "Exibir grupo organizacional", @@ -5884,7 +5884,7 @@ "currentWindow": "Janela atual", "cycle": "Ciclo", "cycleTooltip": "Compare os resultados com o mesmo horário no ciclo anterior.", - "runningFor": "Executando por {período} no intervalo de cada {frequência}", + "runningFor": "Executando por {period} no intervalo de cada {frequency}", "currentWindowCannotBeDeleted": "A janela atual não pode ser excluída", "comparingWith": "Comparando com", "referenceWindow": "Janela de referência", @@ -5951,10 +5951,10 @@ "helpWhyWithoutCode": "“text”: “Severidade: alta — propriedade dos pagamentos”", "helpWhyWithLabel": "Com uma variável", "helpWhyWithDesc": "Um modelo compartilhado faz referência à variável; cada alerta define seu próprio valor aqui.", - "helpWhyWithCode": "“text”: “Severidade: {'{'} severidade {'}'} — de propriedade da equipe {'{'} '}”", + "helpWhyWithCode": "“text”: “Severidade: {'{'} severidade {'}'} — de propriedade da equipe {'{'} team {'}'}”", "helpExampleHeading": "Exemplo", "helpVariablesExampleCaption": "Adicione gravidade e equipe como variáveis e use-as em qualquer lugar do modelo:", - "helpVariablesExampleCode": "{'{'}\n “texto”: “[{'{'} severidade {'}'}] {'{'} alert_name {'}'} — {'{'} alert_count {'}'} eventos\\nPropriedade da equipe {'{'} '}”\n{'}'}", + "helpVariablesExampleCode": "{'{'}\n “text”: “[{'{'} severidade {'}'}] {'{'} alert_name {'}'} — {'{'} alert_count {'}'} eventos\\nPropriedade da equipe {'{'} team {'}'}”\n{'}'}", "helpExampleResultLabel": "O que é enviado", "helpVariablesExampleResult": "{'{'} “texto”: “[alto] Alta CPU — 42 eventos\\nPropriedade de pagamentos” {'}'}", "helpTemplateWhenHeading": "Quando devo cancelar?", @@ -7920,7 +7920,7 @@ "sessions": "sessões", "failedToLoad": "Falha ao carregar sessões", "retry": "Tente novamente", - "tokenTooltip": "Entrada: {entrada} · Saída: {saída} · Total: {total}", + "tokenTooltip": "Entrada: {input} · Saída: {output} · Total: {total}", "durationNs": "ns", "unknownUser": "Usuário desconhecido", "columns": { @@ -7952,7 +7952,7 @@ "searchPlaceholder": "Pesquise rastros nesta sessão...", "turn": "vire", "turns": "gira", - "turnCount": "{contagem} {unidade}", + "turnCount": "{count} {unit}", "openInTraceExplorer": "Exibir rastreamento", "filters": { "status": "Status", @@ -7981,14 +7981,14 @@ "cacheImpact": "Impacto do cache" }, "kpiSub": { - "turns": "Sessão completa · {duração}", - "errorsLead": "{erros} de {total} · Turnos", - "errorsLeadMany": "{erros} de {total} turnos", + "turns": "Sessão completa · {duration}", + "errorsLead": "{errors} de {total} · Turnos", + "errorsLeadMany": "{errors} de {total} turnos", "filterErrors": "Erros de filtro", "resetFilters": "Redefinir filtros", "errorsNone": "Sem erros · {total} turnos", - "latencyLead": "Mais lento {mais lento} ·", - "costLead": "Pico {pico} ·", + "latencyLead": "Mais lento {slowest} ·", + "costLead": "Pico {peak} ·", "tokens": "{ratio}% em cache", "cacheImpact": "{ratio}% Reutilizado · {tokens} Tão lido", "cacheEstimate": "O impacto líquido inclui leituras de cache e custos de criação de cache.", @@ -8055,7 +8055,7 @@ "external": "Externo", "rpc": "RPC" }, - "unhealthyTooltip": "{contagem} {status}", + "unhealthyTooltip": "{count} {status}", "legend": { "title": "Taxa de Erros" } @@ -8676,7 +8676,7 @@ "dimensionBasedCorrelation": "Correlação baseada em dimensões", "tracesFromService": "Traços do serviço: {service}", "noTracesFound": "Nenhum traço correlacionado encontrado", - "noTracesDescription": "Nenhum rastreamento encontrado para o serviço {service} no intervalo de tempo selecionado", + "noTracesDescription": "Nenhum rastreamento encontrado para esta correlação no intervalo de tempo selecionado.", "viewInTraces": "Exibir em traços", "filters": "Filtros", "semanticFieldGroupsCaption": "Defina mapeamentos de nomes de campos para correlação e desduplicação (por exemplo, host, hostname, node → grupo 'host')", @@ -9731,7 +9731,7 @@ "trueFalseTitle": "Verdadeiro versus falso", "legendAvg": "Média", "legendP95": "p95", - "legendThreshold": "limite {direção} {valor}", + "legendThreshold": "limite {direction} {value}", "legendHealthy": "saudável", "legendUnhealthy": "insalubre", "legendHealthyRate": "Taxa saudável", @@ -10466,7 +10466,7 @@ "trace": "Selecione um ou mais scorers para executar para cada trace elegível.", "session": "Selecione um ou mais scorers para executar para cada sessão elegível." }, - "meta": "{tipo} · v {versão}", + "meta": "{type} · v{version}", "placeholder": "Selecionar Avaliadores", "searchPlaceholder": "Pesquisar avaliadores…", "empty": "Nenhum avaliador disponível. Crie um primeiro na aba Avaliadores." diff --git a/web/src/locales/languages/tr-TR.json b/web/src/locales/languages/tr-TR.json index 5b3ea29ef8..7a1e0b99c1 100644 --- a/web/src/locales/languages/tr-TR.json +++ b/web/src/locales/languages/tr-TR.json @@ -388,7 +388,7 @@ }, "filtered": { "title": "{noun} bulunamadı", - "description": "Şu anki filtrelerinizle veya aramanızla eşleşen {isim} yok. Onları ayarlamayı veya temizlemeyi deneyin.", + "description": "Şu anki filtrelerinizle veya aramanızla eşleşen {noun} yok. Onları ayarlamayı veya temizlemeyi deneyin.", "action": "Filtreleri temizle", "actionDesc": "Tüm filtreleri sıfırlayın ve arayın", "fallbackNoun": "sonuçlar" @@ -571,7 +571,7 @@ "attributes": "Öznitellikler", "events": "Olaylar", "goHome": "Eve git", - "redirectingHome": "{geri sayım} saniye içinde eve yönlendiriliyor...", + "redirectingHome": "{countdown} saniye içinde eve yönlendiriliyor...", "name": "İsim", "add": "Ekle", "selectAll": "Tümünü seç", @@ -1115,7 +1115,7 @@ "trendFlat": "düz", "trendNoData": "Seçilen zaman aralığında oluşum yok", "eventsUnit": "etkinlikler", - "firstSeenAgo": "ilk {zaman}", + "firstSeenAgo": "ilk {time}", "errorsOverTime": "Zaman içindeki hatalar", "noErrorsInWindow": "Seçilen zaman aralığında hata yok", "unhandled": "İşlenmemiş", @@ -1128,7 +1128,7 @@ "crashFreeSessions": "Çökmesiz oturumlar", "sessionsHitError": "{totalSessions} oturumlarının {errorSessions} hatasıyla karşılaştı", "usersAffected": "Etkilenen kullanıcılar", - "ofActiveUsers": "{toplam} aktif kullanıcıdan · {pct} %", + "ofActiveUsers": "{total} aktif kullanıcıdan · {pct} %", "newIssues": "Yeni konular", "firstSeenSinceDeploy": "{version} dağıtımından bu yana ilk kez görüldü", "firstSeenInWindow": "ilk olarak seçilen pencerede görüldü", @@ -1220,7 +1220,7 @@ "activity": "Faaliyet", "noActivityBounced": "Etkinlik yok - hemen sıçradı", "eventsCount": "{count} olaylar", - "activityAriaLabel": "{olaylar} olaylar, {hatalar} hatalar, {hayal kırıklıkları} oturum süresi boyunca hayal kırıklıkları", + "activityAriaLabel": "Oturum süresi boyunca {events} olay, {errors} hata, {frustrations} hayal kırıklığı", "timestamp": "Zaman damgası", "type": "Tür", "viewURL": "URL'yi görüntüle", @@ -1724,7 +1724,7 @@ "confirmRejectMsg": "{org} katılma davetini reddetmek istediğinizden emin misiniz?", "expired": "Süresi doldu", "expiresToday": "Bugün Sona Eriyor", - "daysLeft": "{gün} gün kaldı" + "daysLeft": "{days} gün kaldı" }, "serviceAccounts": { "header": "Hizmet Hesapları", @@ -2132,7 +2132,7 @@ "orgBeingDeletedSwitching": "Bu organizasyon siliniyor. Sizi başka bir organizasyona yönlendiriyoruz.", "createOrganization": "Organizasyon Oluştur", "updateOrganization": "Organizasyonu Güncelle", - "makeBilledMember": "Bu Kuruluşu {org} Faturalandırma Üyesi Yapın", + "makeBilledMember": "Bu kuruluşu geçerli kuruluşun faturalandırma üyesi olarak ekleyin.", "add": "Organizasyon Ekle", "save": "Kaydet", "cancel": "İptal", @@ -3784,7 +3784,7 @@ "maximum": "Maksimum", "fixedValue": "Sabit Değer", "legendLabel": "Efsane", - "overrideMessage": "Seri adı geçersiz kılıyor. Örneğin, {endpoint} with, uç nokta için etiket değeri ile değiştirilir.", + "overrideMessage": "Seri adı geçersiz kılmaları.", "overrideMessageExample": "Örneğin, {endpoint} uç nokta için etiket değeri ile değiştirilecektir.", "unsavedMessage": "Kaydedilmemiş değişiklikleriniz var. Gitmek istediğinden emin misin?", "addFieldMessage": "Lütfen listeden bir alan ekleyin", @@ -4834,7 +4834,7 @@ "recentEvents": "Son Olaylar", "investigate": "Araştırın→", "viewAll": "Tümünü görüntüle", - "showingOf": "{total} arasından {gösterilen} gösteriliyor", + "showingOf": "{total} arasından {shown} gösteriliyor", "colErrorRate": "Hata oranı", "colLatency": "Gecikme", "colReqs": "Reqs", @@ -5089,7 +5089,7 @@ "inviteAccepted": "Davet Başarıyla Kabul Edildi", "inviteRejected": "Davet Başarıyla Reddedildi", "plansManagedTitle": "Ana Kuruluş Tarafından Yönetilen Faturalandırma", - "plansManagedDescription": "Bu kuruluşun faturalandırması {payer} tarafından yönetilir. Plan değişiklikleri ana kuruluş tarafından ele alınır.", + "plansManagedDescription": "Bu kuruluşun faturalandırması {name} ({id}) tarafından yönetilir. Plan değişiklikleri ana kuruluş tarafından ele alınır.", "chipPlanManaged": "Ebeveyn Tarafından Yönetilen Plan", "chipNoInvoices": "Ayrı Fatura Yok", "viewOrgGroup": "Organizasyon Grubunu Görüntüle", @@ -5884,7 +5884,7 @@ "currentWindow": "Geçerli pencere", "cycle": "Döngü", "cycleTooltip": "Sonuçları önceki döngüdeki aynı zamanla karşılaştırın.", - "runningFor": "Her {frekans} aralığında {dönem} boyunca çalışıyor", + "runningFor": "Her {frequency} aralığında {period} boyunca çalışıyor", "currentWindowCannotBeDeleted": "Geçerli pencere silinemiyor", "comparingWith": "İle karşılaştırmak", "referenceWindow": "Referans Penceresi", @@ -5951,10 +5951,10 @@ "helpWhyWithoutCode": "“text”: “Ödemelerin ciddiyeti: yüksek — ödemelere aittir”", "helpWhyWithLabel": "Bir değişken ile", "helpWhyWithDesc": "Paylaşılan bir şablon değişkene atıfta bulunur; her uyarı burada kendi değerini belirler.", - "helpWhyWithCode": "“text”: “Önem derecesi: {'{'} ciddiyet {'}'} — {'{'} ekibine ait {'}}”", + "helpWhyWithCode": "“text”: “Önem derecesi: {'{'} ciddiyet {'}'} — {'{'} team {'}'} ekibine ait”", "helpExampleHeading": "Örnek", "helpVariablesExampleCaption": "Değişken olarak önem ve ekip ekleyin, ardından bunları şablonun herhangi bir yerinde kullanın:", - "helpVariablesExampleCode": "{'{'}\n “text”: “[{'{'} ciddiyeti {'}'}] {'{'} alert_name {'}'} — {'{'} alert_count {'}'} olayları\\n{'{'} ekibine ait {'}}”\n{'}'}", + "helpVariablesExampleCode": "{'{'}\n “text”: “[{'{'} ciddiyeti {'}'}] {'{'} alert_name {'}'} — {'{'} alert_count {'}'} olayları\\n{'{'} team {'}'} ekibine ait”\n{'}'}", "helpExampleResultLabel": "Ne gönderilir", "helpVariablesExampleResult": "{'{'} “text”: “[yüksek] Yüksek CPU — 42 olay\\nÖdemelere aittir” {'}'}", "helpTemplateWhenHeading": "Ne zaman geçersiz kılmalıyım?", @@ -5963,7 +5963,7 @@ "helpRowTemplateWhyDesc": "Bir uyarı birçok kayıtla eşleşebilir. Satır şablonu bir kaydı biçimlendirir; {'{'} rows {'}'} daha sonra her biçimlendirilmiş kaydı ana şablona bırakır - böylece bir döngü yazmadan tüm eşleşmeleri listelersiniz.", "helpRowTemplateExampleCaption": "Eşleşen her satır satır şablonu tarafından biçimlendirilir, ardından {'{'} satırları {'}'} hepsini ana şablona bırakır:", "helpRowTemplateExampleRowLabel": "Satır şablonu (bir kaydı biçimlendirir)", - "helpRowTemplateExampleRowCode": "{'{'} pod {'}'}: {'{'} level {'} at {' {'} _zaman damgası {'} '}", + "helpRowTemplateExampleRowCode": "{'{'} pod {'}'}: {'{'} level {'}'} at {'{'} _timestamp {'}'}", "helpRowTemplateExampleMainLabel": "Ana şablon ({'{'} satırlarını kullanır {'}'})", "helpRowTemplateExampleMainCode": "{'{'} “text”: “{'{'} alert_count {'}'} eşleşiyor:\\n{'{'} satır {'}'}” {'}'}", "helpRowTemplateExampleResult": "{'{'} “text”: “2 eşleşmes:\\nweb-1:14:02 'de HATA\\nweb-3:14:05 'te HATA\" {'}'}", @@ -7920,7 +7920,7 @@ "sessions": "oturumları", "failedToLoad": "Oturumlar yüklenemedi", "retry": "Tekrar dene", - "tokenTooltip": "Giriş: {input} · Çıktı: {output} · Toplam: {toplam}", + "tokenTooltip": "Giriş: {input} · Çıktı: {output} · Toplam: {total}", "durationNs": "ns", "unknownUser": "Bilinmeyen kullanıcı", "columns": { @@ -7952,7 +7952,7 @@ "searchPlaceholder": "Bu oturumdaki izleri ara...", "turn": "dönüş", "turns": "sıralar", - "turnCount": "{count} {birim}", + "turnCount": "{count} {unit}", "openInTraceExplorer": "İzlemeyi Görüntüle", "filters": { "status": "Durum", @@ -7981,14 +7981,14 @@ "cacheImpact": "Önbellek Etkisi" }, "kpiSub": { - "turns": "Oturumu Tamamla · {süre}", - "errorsLead": "{total} değerinden {hatalar} · Dönüyor", - "errorsLeadMany": "{toplam} dönüşün {hataları}", + "turns": "Oturumu Tamamla · {duration}", + "errorsLead": "{total} değerinden {errors} · Dönüyor", + "errorsLeadMany": "{total} dönüşün {errors}", "filterErrors": "Filtre Hataları", "resetFilters": "Filtreleri Sıfırla", "errorsNone": "Hata Yok · {total} Dönüş", - "latencyLead": "En yavaş {en yavaş} ·", - "costLead": "Zirve {tepe} ·", + "latencyLead": "En yavaş {slowest} ·", + "costLead": "Zirve {peak} ·", "tokens": "{ratio} %Önbelleğe alındı", "cacheImpact": "{ratio} %Yeniden Kullanıldı · {tokens} Tok Oku", "cacheEstimate": "Net etki, önbellek okumalarını ve önbellek oluşturma maliyetlerini içerir.", @@ -8055,7 +8055,7 @@ "external": "Harici", "rpc": "RPC" }, - "unhealthyTooltip": "{count} {durum}", + "unhealthyTooltip": "{count} {status}", "legend": { "title": "Hata Oranı" } @@ -8676,7 +8676,7 @@ "dimensionBasedCorrelation": "Boyut Tabanlı Korelasyon", "tracesFromService": "Hizmetten gelen izler: {service}", "noTracesFound": "İlişkili iz bulunamadı", - "noTracesDescription": "Seçilen zaman aralığında {service} hizmeti için iz bulunamadı", + "noTracesDescription": "Seçilen zaman aralığında bu korelasyon için iz bulunamadı.", "viewInTraces": "İzlerde Görüntüle", "filters": "Filtreler", "semanticFieldGroupsCaption": "Korelasyon ve tekilleştirme için alan adı eşlemelerini tanımlayın (örn., ana bilgisayar adı, düğüm → 'ana bilgisayar' grubu)", @@ -9721,7 +9721,7 @@ }, "drillTitle": "İzlere delin", "trendTitle": "Eğilim", - "trendSubtitle": "{configName}, {kutu}", + "trendSubtitle": "{configName}, {bucket}", "distributionTitle": "Puan dağılımı", "compositionTitle": "Zamanla kompozisyon", "categoryDistributionTitle": "Kategori dağılımı", @@ -9731,7 +9731,7 @@ "trueFalseTitle": "Doğru vs Yanlış", "legendAvg": "Ort", "legendP95": "p95", - "legendThreshold": "eşik {yön} {değer}", + "legendThreshold": "eşik {direction} {value}", "legendHealthy": "sağlıklı", "legendUnhealthy": "sağlıksız", "legendHealthyRate": "Sağlıklı oranı", @@ -10466,7 +10466,7 @@ "trace": "Her uygun trace için çalıştırılacak bir veya daha fazla puanlayıcı seçin.", "session": "Her uygun oturum için çalıştırılacak bir veya daha fazla puanlayıcı seçin." }, - "meta": "{tür} · v {sürüm}", + "meta": "{type} · v{version}", "placeholder": "Puanlayıcıları Seç", "searchPlaceholder": "Puanlayıcıları ara…", "empty": "Kullanılabilir puanlayıcı yok. Önce Puanlayıcılar sekmesinde bir tane oluşturun." diff --git a/web/src/locales/localeMessages.spec.ts b/web/src/locales/localeMessages.spec.ts new file mode 100644 index 0000000000..e5d496a519 --- /dev/null +++ b/web/src/locales/localeMessages.spec.ts @@ -0,0 +1,187 @@ +// Copyright 2026 OpenObserve Inc. +// +// vue-i18n compiles a message the first time it is rendered (JIT compilation is +// on — see __INTLIFY_JIT_COMPILATION__ in vite.config.ts). A message whose text +// contains raw `{`, `}` or `@` is not a string to vue-i18n: it is interpolation +// or linked-message syntax, and compiling it throws. The throw happens inside +// render, so it does not surface as a nice error — it tears down whatever +// component asked for the message. +// +// That is invisible until the exact branch that renders the message runs. A +// `{"key": "value"}` placeholder on the HTTP check's request-body field only +// renders for non-GET methods, so picking POST made the entire HTTP Request +// card disappear with nothing but `SyntaxError: 2` in the console. +// +// This ran against en-US only, which left the far likelier source uncovered: the +// other 14 locales are machine-translated, and a translator that localises a +// placeholder NAME produces exactly the same crash in exactly the same way. +// +// en-US "Are you sure you want to delete {identifier}?" +// zh-CN "您确定要删除 {标识符} 吗?" -> SyntaxError: 2 +// +// IAM -> Service Accounts rendered blank in Chinese for that reason, and the +// same class also arrives as mangled literal escapes — {'{'}team{'}'} coming +// back as {'{'} '}, an unbalanced brace. +// +// So every locale is compiled here, and every translated message is additionally +// checked to interpolate only names en-US defines. A translated `{Anzahl}` for +// `{count}` compiles fine and then silently renders as an empty string, because +// nothing passes a value under that name. +// +// Literal braces/at-signs must be escaped as {'{'}, {'}'}, {'@'}. + +import { describe, expect, it } from "vitest"; +import { readdirSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { compile } from "@intlify/core-base"; + +const LANGUAGES_DIR = resolve(__dirname, "./languages"); +const SOURCE_LOCALE = "en-US"; + +const localeFiles = readdirSync(LANGUAGES_DIR) + .filter((name) => name.endsWith(".json")) + .sort(); + +function flatten(node: unknown, path = "", out: [string, string][] = []): [string, string][] { + if (typeof node === "string") { + out.push([path, node]); + return out; + } + if (node && typeof node === "object") { + for (const [k, v] of Object.entries(node)) { + flatten(v, path ? `${path}.${k}` : k, out); + } + } + return out; +} + +function loadLocale(stem: string): [string, string][] { + return flatten(JSON.parse(readFileSync(resolve(LANGUAGES_DIR, `${stem}.json`), "utf8"))); +} + +/** + * Compiles `message` and returns the interpolation tokens vue-i18n would resolve + * for it — named (`{count}`), list (`{0}`) and linked (`@:some.key`). + * + * Throws if the message does not compile, so this doubles as the compile check. + * onError rethrows: the compiler otherwise collects errors and returns a + * best-effort result, which would hide exactly what we are looking for. + * + * The names come from *running* the compiled message against a recording + * context rather than from pattern-matching the source text, because only the + * compiler knows which braces are placeholders and which are literal escapes — + * `{'}'}` is one token whose body is the character a `{...}` regex stops at. + * Every branch of a pluralized message is evaluated, so a name used only in the + * plural form still counts. + */ +export function interpolationTokens(message: string, key = "", locale = ""): string[] { + const seen = new Set(); + const render = compile(message, { + key, + locale, + onError: (err) => { + throw err; + }, + // Locale files legitimately contain and friends; that advisory is + // not this test's business and would drown its output. + warnHtmlMessage: false, + }); + + render({ + type: "text", + normalize: (values: unknown[]) => values.join(""), + interpolate: (value: unknown) => String(value), + named: (key: string) => { + seen.add(`{${key}}`); + return ""; + }, + list: (index: number) => { + seen.add(`{${index}}`); + return ""; + }, + linked: (key: string) => { + seen.add(`@:${key}`); + return ""; + }, + message: () => () => "", + plural: (branches: unknown[]) => branches[0], + values: {}, + } as never); + + return [...seen].sort(); +} + +describe("interpolationTokens", () => { + it("reports named, list and linked tokens", () => { + expect(interpolationTokens("Hi {name}, {0} new @:common.alerts")).toEqual([ + "@:common.alerts", + "{0}", + "{name}", + ]); + }); + + it("ignores vue-i18n literal escapes", () => { + expect(interpolationTokens("{'{'}pod{'}'}: {'{'}level{'}'}")).toEqual([]); + }); + + it("sees a name that appears only in a plural branch", () => { + expect(interpolationTokens("{one} day | {many} days")).toEqual(["{many}", "{one}"]); + }); + + it("throws on a placeholder name vue-i18n cannot tokenize", () => { + // The zh-CN Service Accounts crash, reduced. + expect(() => interpolationTokens("您确定要删除 {标识符} 吗?")).toThrow(); + }); +}); + +describe.each(localeFiles)("%s", (file) => { + const stem = file.replace(/\.json$/, ""); + const messages = loadLocale(stem); + + it("should all compile with vue-i18n", () => { + const broken: string[] = []; + for (const [key, value] of messages) { + try { + interpolationTokens(value, key, stem); + } catch (err) { + broken.push(`${key}: ${JSON.stringify(value)} — ${String(err).split("\n")[0]}`); + } + } + expect(broken).toEqual([]); + }); + + if (stem !== SOURCE_LOCALE) { + it("should only interpolate names en-US defines", () => { + const source = new Map(loadLocale(SOURCE_LOCALE)); + const unknown: string[] = []; + + for (const [key, value] of messages) { + const english = source.get(key); + // Keys en-US no longer has are pruned by the translation script, not here. + if (english === undefined) continue; + + let defined: string[]; + let used: string[]; + try { + defined = interpolationTokens(english, key, SOURCE_LOCALE); + used = interpolationTokens(value, key, stem); + } catch { + continue; // already reported by the compile test above + } + + // A subset, not an exact match: dropping a placeholder the target + // language does not need is legitimate (an English plural suffix like + // {plural} has no equivalent in Japanese). Introducing a name en-US + // never passes a value for is always a bug. + const extra = used.filter((token) => !defined.includes(token)); + if (extra.length) { + unknown.push( + `${key}: ${extra.join(", ")} — en-US has ${JSON.stringify(english)}, this has ${JSON.stringify(value)}`, + ); + } + } + + expect(unknown).toEqual([]); + }); + } +});