fix: i18n migration

This commit is contained in:
ktx-vaidehi 2026-08-03 11:22:57 +05:30
parent c85af27745
commit 08b8087190
537 changed files with 5086 additions and 4414 deletions

View File

@ -3,13 +3,28 @@
How the OpenObserve web app guarantees that user-facing text is translatable, and
what to do when a check fires.
**Status: enforcement complete and green.** `lint:ci` 0 errors · `type-check:app`
0 errors · spec-inclusive `type-check` 0 errors · `format:check` clean.
**Status: enforcement complete.** `lint:ci` 0 errors · `type-check:app` **exit 0**
(with `--composite false`, which the npm script passes, no TS6307 surfaces) ·
`format:check` clean.
> **Known measurement gap:** the "spec-inclusive" `type-check` (tsconfig.vitest.json)
> currently checks **no spec files at all** — its include is `src/**/*.spec.{ts,js}`,
> and TypeScript globs do **not** brace-expand, so the pattern matches nothing.
> Any past "spec-inclusive type-check 0 errors" claim was vacuous. Fixing it means
> splitting the include into `src/**/*.spec.ts` + `src/**/*.spec.js` AND then fixing
> the spec-only type errors that surface (e.g. `metricGrouping.spec.ts` assigns keys
> that don't exist in en-US to `I18nKey` fixtures). Tracked in §9.
The mechanisms are done and so is the message cleanup — **0 fake plurals** remain in
en-US. What is left is narrower: **non-English plural rules**, the last **6 `gt` sites**,
and **one CI gap**. See [§9 Outstanding work](#9-outstanding-work) — that section is the
to-do list, kept in sync with measurements.
en-US, the 10 surviving `gt()` calls are each individually justified (§8), `TEXT_ATTRS`
has been retired in favour of prop types (§8), and the dead-key sweep is complete (§9).
What is left is narrower: **non-English plural rules** and **spec type-checking**
(a broken tsconfig glob plus a missing PR gate, §9.3). See
[§9 Outstanding work](#9-outstanding-work) — that section is the to-do list, kept in
sync with measurements.
Converting the 71 dynamic key sites so they can be type-checked was **considered and
declined** — it is recorded under §4 as an accepted limit, not pending work.
---
@ -89,11 +104,11 @@ only two remaining `(s)` values are the seconds unit. Keep it that way.
`{{ t('common.save') }}` · `:label="t('x')"` · `:label="row.name"` (a variable
contributes no literal) · `:label="'—'"` (no letters).
### Two curated lists
### One curated list
`TEXT_ATTRS` is **gone** — prop types now decide what counts as a text prop, so there
is no list to maintain (§8). Declaring a prop `I18nText` is what makes it enforced.
- **`TEXT_ATTRS`** (47 names) — props that carry user-facing text. Feeds both the
static and bound rules, so "what counts as a text prop" is defined once. **Add a
name here when a component takes UI text through a new prop.**
- **`NON_TRANSLATABLE`** (39 entries) — tokens that must read identically in every
language: units (`px`, `ms`, `ns`, `min`), symbols (`×`, `→`, `~`), protocol and
spec identifiers (`GET`, `UTC`, `SQL`, `PromQL`, the OpenTelemetry statuses
@ -121,7 +136,7 @@ follows a pattern the library already used for icons (`iconLeft?: IconName`).
| **`I18nKey`** | a field holding an **i18n key as data** (`titleKey`, `labelKey`) |
| **`useI18nTyped()`** | replaces `useI18n()` in components — `t` returns `I18nText` |
| **`TranslateFn`** | the type of `t` when a composable/util takes it as a **parameter** |
| **`gt()`** | last resort for genuinely context-free code — **1 file**, see below |
| **`gt()`** | last resort for genuinely context-free code — **4 files**, see below |
| **`raw()`** | the explicit opt-out for text that must not be translated |
```ts
@ -155,7 +170,7 @@ presets.ts(76,5): error TS2820: Type '"emptyState.noLogs.titel"' is not assignab
to type 'I18nKey'. Did you mean '"emptyState.noLogs.title"'?
```
Cost: **~31 s** for the full app type-check with the 10,665-key union. Not a
Cost: **~31 s** for the full app type-check with the ~9,800-key union. Not a
perf concern.
### Getting a `t`: three cases, in order of preference
@ -179,7 +194,7 @@ perf concern.
}
```
This is the standard for non-component code (**27 files**). It is preferred over
This is the standard for non-component code (**35 files**). It is preferred over
`gt()` because these functions are also called directly by specs, outside any
component, where `useI18n()` would throw. Specs pass the real translator:
@ -192,13 +207,27 @@ perf concern.
composable translates, `t` belongs on that function, not on the composable.
3. **Genuinely context-free code**`gt("some.key")`. Only where there is neither a
setup context nor a caller to thread `t` through. Exactly **one** file qualifies:
`useUnauthorizedErrorGrouper.ts`, reached from an axios 403 interceptor registered
once at module load (interceptor → 300 ms `setTimeout` → toast → click handler).
setup context nor a caller to thread `t` through. Exactly **four** files qualify
(10 call sites), each for a distinct structural reason:
- `useUnauthorizedErrorGrouper.ts` (6) — reached from an axios 403 interceptor
registered once at module load (interceptor → 300 ms `setTimeout` → toast →
click handler). The founding case.
- `ingestion/setupCard/content/kubernetes.ts` (2) — the setup-card registry pins
every builder to `(subs: CardSubstitutions) => RichCardContent`; threading `t`
would change that shared contract for ~20 cards. Static prose in cards uses
`descriptionKey`/`helpKey` (renderer-resolved); `gt` covers only the one
description that interpolates a URL, translated at card-build time.
- `utils/query/searchError.ts` (1) — the translated **default** of an optional
`fallback: I18nText` parameter, evaluated per call; callers that hold `t`
override it (e.g. `parseSearchError(err, gt("search.unknownError"))`).
- `usePanelPromQLExecutor.ts` (1) — reached through composable chains that are
not guaranteed to run in a setup context, where `useI18n()` would throw.
`gt` is deliberately a narrow escape hatch, not an alternative to case 2. Before
reaching for it, check whether a caller can pass `t` instead — 43 of the original
49 `gt` sites turned out to be case 2.
reaching for it, check whether a caller can pass `t` instead — the overwhelming
majority of historical `gt` sites (including the import validators and
`useQualityDetailCharts`, converted since) turned out to be case 2.
> **Keys resolved at display time, not module load.** When storing keys as data, keep
> the `I18nKey` map at module scope but resolve it inside the function that renders.
@ -250,11 +279,18 @@ annotation is made by hand at the declaration rather than by a pattern match.
| `t()` keys — literal | enforced (ESLint, `.vue` **and** `.ts`) |
| i18n keys stored as data | enforced (`I18nKey`, 19 files) |
| Toast / notification text | enforced (`I18nText`, incl. the store + wrappers) |
| Text reached via `useI18nTyped()` | branded across **663** files |
| Text reached via `useI18nTyped()` | branded across **668** files |
| Composables / utils taking `t: TranslateFn` | **27** files |
**Numbers:** en-US grew 10,216 → **10,665 keys**; **311** sit under `toastMessages.*`
(28 module groups). 62 `raw()` opt-outs. **6** `gt()` call sites, all in one file.
**Numbers** (re-measured 2026-07-31, after the post-review debt cleanup): en-US holds
**9,837 keys** across **72 namespaces** (10,216 → 11,002 as text was migrated in,
→ 9,648 after the dead-key sweep §9, → 9,837 as the remaining hardcoded prose —
import validators, setup-card descriptions, status labels, dialog buttons — was
keyed); **309** sit under `toastMessages.*`. **977** `raw()` opt-outs — the count
rose steeply when props became `I18nText`, since every genuinely non-translatable
value (units, tokens, glyphs, API data) has to say so explicitly, then fell as
`raw()`-wrapped prose was converted to keys. **10** `gt()` call sites in **4**
files (see §3 case 3 for the per-file justification).
**Where the two checkers divide.** `no-missing-keys` validates `t('x.y')` **calls**;
it cannot see a key assigned to a field. Keys stored as **data** are validated instead
@ -264,27 +300,54 @@ be green to claim key coverage; neither alone is sufficient.
### Not covered
- **Dynamic keys** — ``t(`about.feature_${id}`)`` (**312** sites). No lint or type
check can resolve these. The `t` key parameter is deliberately permissive
(`I18nKey | (string & {})`) so they keep compiling.
- **Dynamic keys — 71 sites. Considered and DECLINED; do not re-open as pending work.**
``t(`about.feature_${id}`)`` and similar. A renamed or deleted key reached this way is
caught by nothing: ESLint sees no literal, `t`'s permissive
`I18nKey | (string & {})` parameter accepts any string, vue-i18n returns the key
instead of throwing, and the production build strips its dev warning. It renders the
raw dotted path on screen.
Nothing is currently broken — every key these sites can request was expanded and
verified present. The exposure is a future rename, concentrated in `onlineEvals`
(40 of the 71 sites).
Closing it would mean removing the `(string & {})` arm, which surfaces **117 errors**:
~80 are unrelated consumers declaring their translator loosely as
`(key: string) => string` (a contravariance failure, not a key problem), 15 need a
variable widened to a literal union, 9 need a `Record<string, I18nKey>` map, and a
handful are genuine key/code mismatches. Two-thirds of the work is therefore not about
dynamic keys at all, which is why this was judged not worth it as a standalone project.
If the guarantee is ever wanted, the cheap route is a second strict export
(`(key: I18nKey)`) used by new code only — no flag day, no 98-file PR.
> Count these with a word boundary: `grep 't(\`'` also matches `` fetch(`…`) `` and any
other call ending in `t`, which inflates the figure roughly fourfold. A further 21
> sites use backticks but interpolate nothing — those are ordinary static keys.
- **Interfaces not yet annotated.** `I18nText` guards what it is applied to. That is
_incomplete_, never _wrong_ — coverage grows one declaration at a time, at the
definition site, with no central registry to keep in sync.
- **Other locales.** The 14 non-en locales are generated from en-US and lag behind;
`localeDir` points at en-US only, on purpose. Never hand-edit them.
- **Unused keys.** `@intlify/vue-i18n/no-unused-keys` is available but **not
enabled**. Measured on this repo it reports 2,421 keys of which only ~1,302 are
genuinely dead — 681 are reached via a key-string, 437 via a dynamic prefix. If
you enable it, use `warn`, never `enableFix`, and populate `ignores` with the
dynamic prefixes; its autofix would delete live translations.
- **Unused keys.** `@intlify/vue-i18n/no-unused-keys` is available but **not enabled**.
It reports 2,421 keys, but it only sees `t('literal')` calls — so keys reached
through a variable or a dynamic prefix look unused to it, and its autofix would
delete them. A direct measurement (literal tokens + dynamic prefixes + the one
concatenation site) put the genuinely dead set at **1,363**, since deleted (§9).
If you ever enable the rule, use `warn`, never `enableFix`, and populate `ignores`
with the dynamic prefixes. Note that even the direct measurement missed 9 live keys
(§9) — no automated sweep sees keys held in JSON data files.
---
## 5. Deliberately deferred: `strictTemplates`
Typing a component prop `label: I18nText` only gates `<OButton label="Save" />` if
Vue's `strictTemplates` is on. It is **not** enabled, which is why `TEXT_ATTRS`
still exists.
A **declared** prop typed `label: I18nText` gates `<OButton label="Save" />` with or
without `strictTemplates` — declared props are always checked. `strictTemplates`
governs **undeclared** attributes only. This was verified by probe, and it is what
allowed `TEXT_ATTRS` to be retired (§8) while `strictTemplates` stays off.
Measured twice on this branch:
@ -297,10 +360,9 @@ Of those, ~303 are genuine `TS2322` type mismatches (real latent bugs); the rest
undeclared pass-through attributes.
**It was left out on purpose.** It is a large _type-safety_ migration, not i18n work,
and its i18n payoff is only retiring the `TEXT_ATTRS` list — whose real gap was
measured at **five sites** (all fixed here: `reveal-tooltip`, `hide-tooltip`,
`unstable-dimension-tooltip`, `date-disabled-tooltip`). Worth doing as its own PR;
not worth burying this one under 2,655 unrelated errors.
and it turned out to carry **no i18n payoff at all** — retiring `TEXT_ATTRS` did not
require it. Worth doing as its own PR for the ~303 genuine `TS2322` mismatches; not
worth burying this one under 2,655 unrelated errors.
---
@ -320,8 +382,17 @@ not worth burying this one under 2,655 unrelated errors.
| A recurring unit/symbol/code token | add to `NON_TRANSLATABLE` with a one-line reason |
| A whole file that is genuinely code | add to the SyntaxGuide exemption block |
New text-carrying **component prop** → add its name to `TEXT_ATTRS`.
New text-carrying **component prop** → declare it `I18nText`. In `<script setup>`,
`defineProps<{ label: I18nText }>()`. In the Options API the double cast is required,
because `StringConstructor` resolves to an unbranded `string`:
```ts
title: { type: String as unknown as PropType<I18nText>, default: raw("") }
```
New **interface field** carrying text or a key → declare it `I18nText` / `I18nKey`.
New **i18n key stored in a `.json` data file** → add it to the `localeKeys.spec.ts`
guard; no type or lint rule can see it (§9).
---
@ -397,10 +468,25 @@ Recorded so they are not re-reported as open:
- **Verb injection.** 5 messages interpolated an English verb into a sentence
(`"Failed to {action} the alert"`), which is unlocalisable word order; each is now a
complete message per case.
- **`gt` proliferation.** 49 sites / 22 files → **6 sites / 1 file**, by passing
`t: TranslateFn` (§3, case 2).
- **`gt` proliferation.** 49 sites / 22 files → **10 sites / 4 files**, by passing
`t: TranslateFn` (§3, case 2). The audit (2026-07-31) converted every site whose
caller could supply `t` — the onlineEvals import validators now take `t` via their
`ctx`, `useQualityDetailCharts` and `computePrefixAssignment` take it as a
parameter. The 10 that remain are each structurally unable to receive a `t`
(axios interceptor, fixed registry contract, optional-param default, non-setup
composable chain — the per-file list is in §3 case 3) and are **intentional and
final**. Two alternatives were considered and rejected: deferring keys to render
time, and injecting a translator at the composition root. `gt` stays exported as
a deliberate, narrow escape hatch; **do not re-raise this as cleanup.**
- **The `useI18n` import ban** is clean tree-wide. The only remaining `vue-i18n` import
outside the exempt paths is `createI18n` in a test fixture, which the ban permits.
- **`TEXT_ATTRS` is gone.** The 47-name attribute allowlist and the bound-prop half of
`local/no-bare-bound-text-props` were deleted; prop types now do that job (§3). A
declared `I18nText` prop is checked in templates **without** `strictTemplates`
`strictTemplates` governs _undeclared_ attributes only, so §5 is not a blocker for
this. Type checking is also _stricter_ than the old rule: it rejects a plain `string`
variable, which the rule allowed. `vue/no-bare-strings-in-template` stays enabled,
because a bare text node has no prop to annotate; `NON_TRANSLATABLE` still feeds it.
- **Fake plurals everywhere else.** The remaining 104 `(s)` messages are gone — pipe
plurals went 53 → **139**. Breakdown of how they were fixed:
- 69 already interpolated `{count}`/`{n}`, so the call sites were already correct and
@ -424,9 +510,50 @@ Recorded so they are not re-reported as open:
## 9. Outstanding work
Ordered by value. Item 3 is the cheapest; item 1 is the only user-visible one.
### ~~1. Delete dead keys from `en-US.json`~~ — done
### 1. Non-English plural rules (`pluralRules`)
`en-US.json` went from **11,002 → 9,648 keys** (75 → 71 namespaces, ~90 KB smaller).
1,363 keys were deleted and **9 were restored** after audit; see below. (The count
has since grown again to **9,837 / 72 namespaces** as the post-review debt cleanup
keyed the remaining hardcoded prose — that growth is live keys, not resurrected
dead ones.)
**What the scan excluded up front.** Keys with a literal reference in prod code or
specs, keys reachable through a dynamic prefix (a conservative exclusion — spared
because a dynamic key _might_ reach them, not because they are proven live), and the
5 keys reachable through the one concatenation site (`utils/common.ts`, `"message." +
code`).
**The audit that followed, and why it mattered.** Six independent read-only agents
re-checked all 1,363 deletions, each told to hunt specifically for what a
quoted-literal scan structurally cannot see. They found **9 genuine false deletions**
across two distinct blind spots — both now closed by guards that were verified to
fail when the key is removed:
| Blind spot | Keys | Guard added |
| ----------------------------------------------------------------- | ---- | ------------------------------------------------------------- |
| Keys living in a **JSON data file** (`constants/features.json`) | 5 | `localeKeys.spec.ts` — resolves every key features.json cites |
| Template literal **assigned to a variable** before reaching `t()` | 4 | `useGreeting.ts``const key: I18nKey = …` |
The first is the more important lesson: TypeScript widens imported JSON string values
to `string`, so keys stored in a data file cannot be typed as `I18nKey` and are
invisible to _every_ static check. Annotating `FeatureAvailability` documents intent
but **does not enforce**`FEATURE_REGISTRY` casts with `as`, and the widening defeats
it regardless. Only the spec guard actually catches this.
**If you delete keys again.** Regenerate the list (the set moves as code changes) and
scan **`.json`/`.md`/`.yaml` as well as `.ts`/`.vue`** — the original sweep read only
JS/TS and that is exactly how the features.json keys were lost. Never use
`no-unused-keys --fix`: it shares the blind spot and would delete the dynamic-reachable
keys. `no-missing-keys` and `I18nKey` together catch an over-delete of any _literal_
reference, but neither sees the two cases above.
**Other locales.** The 14 non-English files still carry ~1,317 of the deleted keys.
This is expected and self-healing: `scripts/translations/README.md` documents the
pipeline as pruning keys removed from `en-US.json`. Harmless meanwhile — nothing
requests them and en-US is the fallback.
### 2. Non-English plural rules (`pluralRules`)
The en-US messages are now real pipe plurals, but **branch selection still uses
vue-i18n's built-in default for every locale** because this repo configures no
@ -444,26 +571,47 @@ generated and must not be hand-edited, so they keep their `(s)` until regenerate
This degrades safely: if en-US has a pipe and fr-FR does not, vue-i18n returns the
French message unchanged rather than mis-selecting a branch.
### 2. The last 6 `gt()` sites
### 3. Spec type-checking does not exist — anywhere
`useUnauthorizedErrorGrouper.ts`. Blocked on architecture, not effort — see §3 case 3
for why there is no caller to thread `t` through. Two designs were considered and
rejected (deferring keys to render time; injecting a translator at the composition
root). Closing this is what would allow `gt` to be deleted from `types/i18n.ts`
entirely.
Two separate problems compound here.
### 3. Spec-inclusive `type-check` does not gate PRs
**First, the config is broken.** `tsconfig.vitest.json`'s include is
`src/**/*.spec.{ts,js}` — but TypeScript globs do **not** support brace expansion,
so the pattern matches **zero files**. `npm run type-check` "passes" in seconds
because it checks nothing but `vitest.config.ts`/`vite.config.ts` (verify with
`npx tsc -p tsconfig.vitest.json --listFilesOnly`). Any past claim that the tree
is "green under both configs" was vacuous. Additionally, the npm script's
`NODE_OPTIONS=…` prefix is POSIX-only, so `npm run type-check` errors outright on
Windows shells.
`unit-tests.yml` runs `type-check:app`, which **excludes specs**. The spec-inclusive
`type-check` runs only in `npm-update.yml`, a scheduled dependency job — so a spec that
fails to compile can reach `main`. Adding one step to `unit-tests.yml` closes it, and
the tree is currently green under both, so it would go in clean.
**Second, even a working spec type-check would not gate PRs.** `unit-tests.yml`
(the PR gate) runs only `type-check:app`, which excludes specs. Vitest transpiles
through esbuild, which strips types without checking them, so a type-broken spec
still runs and can pass while testing the wrong thing. This branch hit exactly
that — when `useDashboardPanelData()` gained its `t` parameter, **55 specs** kept
the old signature while `type-check:app` reported 0 errors.
Fix is two steps, in order:
1. Split the include in `tsconfig.vitest.json` into `src/**/*.spec.ts` and
`src/**/*.spec.js`, run it, and fix what surfaces — spec-only type errors exist
today (e.g. `metricGrouping.spec.ts` assigns non-existent keys to `I18nKey`
fixtures), so expect this to start **red**, not clean.
2. Then add the step next to the existing one in `unit-tests.yml`:
```yaml
- name: Type-check specs (any error fails)
working-directory: web
run: npm run type-check
```
### 4. Standing limits (accepted, not scheduled)
- **312 dynamic keys** — unresolvable by design (§4).
- **`no-unused-keys` not enabled** — ~1,302 genuinely dead keys of the 2,421 it
reports; its autofix would delete live translations (§4).
- **71 dynamic keys** — conversion considered and **declined**; see §4 for the full
reasoning and what closing it would cost.
- **`no-unused-keys` not enabled** — it reports 2,421 keys but shares the blind spot
that makes ~1,000 of those false positives, and its autofix deletes what it flags.
The dead-key cleanup in §9.1 uses a directly-measured list instead (§4).
- **`strictTemplates` deferred** — 2,655 errors, a type-safety migration rather than
i18n work (§5).
- **`eslint.config.js` duplicate rule keys** — `prettier/prettier` appears 3×,

View File

@ -73,68 +73,6 @@ const noLegacyO2Tokens = {
},
};
// User-facing text props. `vue/no-bare-strings-in-template` checks these as STATIC
// attributes (label="Save"); the custom rule below checks the SAME names as BOUND
// literals (:label="'Save'" / :label=`Save`). One list feeds both, so "what counts
// as translatable text passed to a component" is defined in exactly one place —
// add a prop name here when a component takes user-facing text through a new prop.
// The a11y/native entries reproduce the built-in rule's defaults (we replace, not
// extend, its attribute map). Component props are evidence-based (scan of web/src).
const TEXT_ATTRS = [
"title",
"aria-label",
"aria-placeholder",
"aria-roledescription",
"aria-valuetext",
"alt",
"label",
"sub-label",
"sublabel",
"placeholder",
"hint",
"tooltip",
"message",
"content",
"help-text",
"caption",
"description",
"subtitle",
"header",
"empty-label",
"error-message",
"button-label",
"primary-button-label",
"secondary-button-label",
"confirm-text",
"cancel-text",
// Additional O2/app text props, discovered by scanning web/src for every
// `:prop="t(...)"` — a prop a dev already wraps in t() is by definition
// translatable text, so its STATIC / bound-literal form must be guarded too.
// Re-run that scan when adding text-carrying props and keep this in sync.
"sub-title",
"footer-title",
"dirty-title",
"action-label",
"ok-label",
"firing-label",
"neutral-button-label",
"filter-label",
"empty-message",
"no-match-text",
"search-placeholder",
"ai-placeholder",
"ai-tooltip",
"disable-ai-reason",
"full-time-prefix",
"legend-healthy",
"legend-avg",
"reveal-tooltip",
"hide-tooltip",
"unstable-dimension-tooltip",
"date-disabled-tooltip",
];
const TEXT_ATTR_SET = new Set(TEXT_ATTRS);
// Non-translatable literal tokens — code/syntax/units that must stay identical in
// every language (a CSS unit, a fixed filename, a documented template-variable token).
// Fed to BOTH i18n rules so they pass WITHOUT a scattered inline eslint-disable
@ -224,10 +162,12 @@ const BARE_STRING_DEFAULT_ALLOWLIST = [
// Bans hardcoded text left directly in a <template> that the built-in
// `vue/no-bare-strings-in-template` (STATIC attrs + text nodes only) can't see:
// • a BOUND text prop — :label="'Save'" / :label=`Go`
// • a v-text / v-html literal — v-text="'Save'"
// • a text interpolation — {{ 'Save' }}
// so a dev can't dodge the check by adding a `:`, a v-text, or mustaches. t()-bound
// so a dev can't dodge the check with a v-text or mustaches. (Bound text PROPS are
// no longer checked here — every text prop is declared I18nText, which rejects a
// bare literal, a composed expression, AND a plain string variable at type-check.)
// t()-bound
// and variable-bound values are expressions (not bare literals) so they correctly
// pass; a literal with no letters (punctuation like '—') is skipped. Only BARE
// literals are caught — composed expressions (:label="'a'+b", ternaries, `${x} y`)
@ -298,12 +238,10 @@ noLegacyO2Tokens.rules["no-bare-bound-text-props"] = {
const text = bareText(node.value && node.value.expression);
if (text == null) return;
if (dir === "bind") {
const arg = node.key.argument; // only :prop / v-bind:prop in the text-attr set
if (!arg || arg.type !== "VIdentifier" || !TEXT_ATTR_SET.has(arg.name)) return;
context.report({
node,
message: `Hardcoded text "${text}" in bound prop :${arg.name} — use t('...') with a key in en-US.json.`,
});
// Bound text props are now guarded by the TYPE, not by a name list: a
// prop declared `I18nText` rejects a bare literal (and a plain string
// variable) at type-check. See src/types/i18n.ts.
return;
} else if (dir === "text" || dir === "html") {
context.report({
node,
@ -399,19 +337,24 @@ export default [
"@intlify/vue-i18n/no-missing-keys": "error",
//
// `no-bare-strings-in-template` (ERROR): no user-facing string typed straight
// into a <template> — text nodes AND static text props (label="Save"). We
// REPLACE the built-in attribute map with TEXT_ATTRS so component props, not
// just native title/alt/placeholder, are covered. Bound props (:label="'x'")
// are caught by the local rule below. (@intlify's own `no-raw-text` is NOT
// used — it flags ~1800 literals/punctuation and is too noisy to gate.)
// into a <template> TEXT NODE. (@intlify's own `no-raw-text` is NOT used —
// it flags ~1800 literals/punctuation and is too noisy to gate.)
//
// Props are NOT listed here any more. Every text-carrying prop is declared
// `I18nText` (src/types/i18n.ts), so `label="Save"`, `:label="'Save'"`,
// composed expressions, and even a plain `string` variable are all rejected
// by `type-check:app` — strictly more than the old hand-maintained name list
// could catch, and with nothing to keep in sync. A text NODE has no prop to
// annotate, which is why this rule still runs.
"vue/no-bare-strings-in-template": [
"error",
{
attributes: { "/.+/": TEXT_ATTRS },
attributes: {},
allowlist: [...BARE_STRING_DEFAULT_ALLOWLIST, ...NON_TRANSLATABLE],
},
],
// The bound-prop half of the same rule (see TEXT_ATTRS above).
// Text-position interpolation `{{ 'Save' }}` and v-text/v-html only; the
// bound-prop half retired with TEXT_ATTRS (the type covers it).
"local/no-bare-bound-text-props": "error",
//
// `t` must come from the typed wrapper, otherwise it returns an unbranded

View File

@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:class="isAnimating ? 'auto-refresh-icon--spinning' : ''"
size="sm"
/>
<OTooltip :content="`${t('search.autoRefresh')}: ${selectedLabel}`" />
<OTooltip :content="raw(`${t('search.autoRefresh')}: ${selectedLabel}`)" />
</OButton>
</template>
<div class="w-75 p-2">
@ -76,7 +76,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
side="right"
align="center"
max-width="18.75rem"
:content="minRangeRestrictionMessageVal"
:content="raw(minRangeRestrictionMessageVal)"
/>
{{ item.label }}
</OButton>
@ -152,7 +152,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
side="right"
align="center"
max-width="18.75rem"
:content="minRangeRestrictionMessageVal"
:content="raw(minRangeRestrictionMessageVal)"
/>
{{ item.label }}
</OButton>
@ -174,7 +174,7 @@ import {
onMounted,
type PropType,
} from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useRouter } from "vue-router";
import { generateDurationLabel } from "../utils/date";
import OButton from "@/lib/core/Button/OButton.vue";
@ -362,6 +362,7 @@ export default defineComponent({
});
return {
raw,
t,
router,
btnRefreshInterval,

View File

@ -58,6 +58,7 @@ import {
onActivated,
watch,
computed,
type PropType,
} from "vue";
import type * as MonacoEditor from "monaco-editor/esm/vs/editor/editor.api";
@ -86,7 +87,7 @@ import { useTheme } from "@/composables/useTheme";
import { debounce } from "lodash-es";
import searchState from "@/composables/useLogs/searchState";
import { useNLQuery } from "@/composables/useNLQuery";
import { useI18nTyped, raw } from "@/types/i18n";
import { type I18nText, useI18nTyped, raw } from "@/types/i18n";
import useNotifications from "@/composables/useNotifications";
import { getImageURL } from "@/utils/zincutils";
import { isAuthError } from "@/utils/authErrors";
@ -168,8 +169,8 @@ export default defineComponent({
default: false,
},
disableAiReason: {
type: String,
default: "",
type: String as unknown as PropType<I18nText>,
default: raw(""),
},
},
emits: [

View File

@ -7,8 +7,8 @@
data-test="confirm-dialog-provider"
size="sm"
:title="currentDialog.title"
:primary-button-label="currentDialog.confirmLabel || t('common.ok')"
:secondary-button-label="currentDialog.cancelLabel || t('common.cancel')"
:primary-button-label="raw(currentDialog.confirmLabel) || t('common.ok')"
:secondary-button-label="raw(currentDialog.cancelLabel) || t('common.cancel')"
:persistent="currentDialog.persistent ?? true"
@click:primary="handleConfirm"
@click:secondary="handleCancel"
@ -21,7 +21,7 @@
<script setup lang="ts">
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import { useConfirmDialog } from "@/composables/useConfirmDialog";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
const { currentDialog, handleConfirm, handleCancel, handleUpdateOpen } = useConfirmDialog();

View File

@ -31,7 +31,14 @@ const mockStore = createStore({
const mockI18n = createI18n({
locale: "en",
messages: {
en: {},
// The component passes t("common.*") messages to copyToClipboard, so
// mounts using this mock need the keys or t() echoes the key path.
en: {
common: {
contentCopiedSuccessfully: "Content Copied Successfully!",
copyContentError: "Error while copying content.",
},
},
},
});
@ -119,7 +126,7 @@ describe("CopyContent.vue Branch Coverage", () => {
expect.any(Function),
{
successMessage: "Content Copied Successfully!",
errorMessage: "Error while copy content.",
errorMessage: "Error while copying content.",
timeout: 5000,
},
);
@ -153,7 +160,7 @@ describe("CopyContent.vue Branch Coverage", () => {
expect.any(Function),
{
successMessage: "Content Copied Successfully!",
errorMessage: "Error while copy content.",
errorMessage: "Error while copying content.",
timeout: 5000,
},
);
@ -323,7 +330,7 @@ describe("CopyContent.vue Branch Coverage", () => {
expect.any(Function),
{
successMessage: "Content Copied Successfully!",
errorMessage: "Error while copy content.",
errorMessage: "Error while copying content.",
timeout: 5000,
},
);

View File

@ -37,8 +37,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
// @ts-nocheck
import { defineComponent, ref } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { defineComponent, ref, type PropType } from "vue";
import { type I18nText, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import { copyToClipboard } from "@/utils/clipboard";
import { maskText, b64EncodeStandard } from "../utils/zincutils";
@ -51,7 +51,7 @@ export default defineComponent({
components: { OButton, OIcon, OTooltip },
props: {
content: {
type: String,
type: String as unknown as PropType<I18nText>,
default: "", // Default value for content prop (empty string in this case)
},
displayContent: {
@ -88,8 +88,8 @@ export default defineComponent({
const copyToClipboardFn = () => {
const content = replaceValues(props.content, false);
copyToClipboard(content, t, {
successMessage: "Content Copied Successfully!",
errorMessage: "Error while copy content.",
successMessage: t("common.contentCopiedSuccessfully"),
errorMessage: t("common.copyContentError"),
timeout: 5000,
});
};

View File

@ -98,7 +98,7 @@ import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import type { DropdownAlign } from "@/lib/overlay/Dropdown/ODropdown.types";
import { ref, reactive, watch, computed } from "vue";
import type { PropType } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
const { t } = useI18nTyped();
@ -106,7 +106,7 @@ const { t } = useI18nTyped();
type PeriodKey = "s" | "m" | "h" | "d" | "w" | "M";
interface RelativePeriod {
label: string;
label: I18nText;
value: PeriodKey;
}
@ -137,7 +137,7 @@ const picker = reactive({
relative: {
value: 0,
period: "m",
label: "Minutes",
label: t("common.minutes"),
},
},
},
@ -145,12 +145,12 @@ const picker = reactive({
// Periods for selection
const relativePeriods: RelativePeriod[] = [
{ label: "Seconds", value: "s" },
{ label: "Minutes", value: "m" },
{ label: "Hours", value: "h" },
{ label: "Days", value: "d" },
{ label: "Weeks", value: "w" },
{ label: "Months", value: "M" },
{ label: t("common.seconds"), value: "s" },
{ label: t("common.minutes"), value: "m" },
{ label: t("common.hours"), value: "h" },
{ label: t("common.days"), value: "d" },
{ label: t("common.weeks"), value: "w" },
{ label: t("common.months"), value: "M" },
];
const relativeDates: Record<PeriodKey, number[]> = {
@ -164,18 +164,18 @@ const relativeDates: Record<PeriodKey, number[]> = {
// Options for custom period input
const relativePeriodsSelect = ref([
{ label: "Seconds", value: "s" },
{ label: "Minutes", value: "m" },
{ label: "Hours", value: "h" },
{ label: "Days", value: "d" },
{ label: "Weeks", value: "w" },
{ label: "Months", value: "M" },
{ label: t("common.seconds"), value: "s" },
{ label: t("common.minutes"), value: "m" },
{ label: t("common.hours"), value: "h" },
{ label: t("common.days"), value: "d" },
{ label: t("common.weeks"), value: "w" },
{ label: t("common.months"), value: "M" },
]);
// Function to map period values to their labels
const getPeriodLabelFromValue = (periodValue: SelectModelValue | string) => {
const period = relativePeriods.find((p) => p.value === periodValue);
return period ? period.label : "Minutes";
return period ? period.label : t("common.minutes");
};
// Watch modelValue to reflect the correct offset when passed in from parent
@ -212,13 +212,20 @@ const updateCustomPeriod = (newPeriod: SelectModelValue | string) => {
emit("update:modelValue", `${picker.data.selectedDate.relative.value}${newPeriod}`);
};
// Display the current selected offset
// Display the current selected offset. Built from a parameterised message
// rather than concatenation word order around the value differs by language.
const getDisplayValue = () => {
return `${picker.data.selectedDate.relative.value} ${picker.data.selectedDate.relative.label} ago`;
return t("common.relativeTimeAgo", {
value: picker.data.selectedDate.relative.value,
unit: picker.data.selectedDate.relative.label,
});
};
const getTrimmedDisplayValue = () => {
return `Past ${picker.data.selectedDate.relative.value} ${picker.data.selectedDate.relative.label}`;
return t("common.relativeTimePast", {
value: picker.data.selectedDate.relative.value,
unit: picker.data.selectedDate.relative.label,
});
};
// Check if the current selection matches the modelValue
@ -234,7 +241,7 @@ const getPeriodLabel = () => {
const selectedPeriod = relativePeriods.find(
(p) => p.value === picker.data.selectedDate.relative.period,
);
return selectedPeriod ? selectedPeriod.label : "Minutes";
return selectedPeriod ? selectedPeriod.label : t("common.minutes");
};
const computedClass = computed(() => {

View File

@ -140,7 +140,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
side="right"
align="center"
max-width="300px"
:content="queryRangeRestrictionMsg"
:content="raw(queryRangeRestrictionMsg)"
/>
</OButton>
</div>
@ -155,7 +155,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
align="center"
max-width="300px"
v-if="queryRangeRestrictionInHour > 0"
:content="queryRangeRestrictionMsg"
:content="raw(queryRangeRestrictionMsg)"
/>
<div class="flex min-w-0 flex-1 gap-2">
@ -195,7 +195,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
align="center"
max-width="300px"
v-if="queryRangeRestrictionInHour > 0"
:content="queryRangeRestrictionMsg"
:content="raw(queryRangeRestrictionMsg)"
/>
<div class="flex justify-center px-3 py-2">
<ODateRangeCalendar
@ -315,7 +315,7 @@ import {
import { copyToClipboard } from "@/utils/clipboard";
import { toast } from "@/lib/feedback/Toast/useToast";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { toZonedTime, fromZonedTime } from "date-fns-tz";
interface ConsumableDateTime {
@ -466,7 +466,7 @@ export default defineComponent({
const isTimezoneSelectOpen = ref(false);
const timezoneSelectOptions = computed(() =>
timezoneOptions.map((tz: string) => ({ label: tz, value: tz })),
timezoneOptions.map((tz: string) => ({ label: raw(tz), value: tz })),
);
let relativePeriods = [
@ -1283,6 +1283,7 @@ export default defineComponent({
};
return {
raw,
t,
menuOpen,
onMenuOpenChange,

View File

@ -67,11 +67,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script lang="ts">
import { defineComponent, computed, inject } from "vue";
import { defineComponent, computed, inject, type PropType } from "vue";
import { useStore } from "vuex";
import { useRouter, RouterLink } from "vue-router";
import { useTheme } from "@/composables/useTheme";
import { useI18nTyped } from "@/types/i18n";
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import { RailIndicatorActiveKey } from "@/lib/core/Navbar/ONavbar.types";
@ -80,13 +80,13 @@ export default defineComponent({
components: { OIcon },
props: {
title: {
type: String,
type: String as unknown as PropType<I18nText>,
required: true,
},
caption: {
type: String,
default: "",
type: String as unknown as PropType<I18nText>,
default: raw(""),
},
link: {

View File

@ -31,7 +31,7 @@
:sideOffset="8"
side="bottom"
align="center"
:content="displayedTitle"
:content="raw(displayedTitle)"
/>
</span>
<OIcon name="arrow-drop-down" size="md" class="flex-shrink-0" />
@ -220,7 +220,7 @@
v-model:open="showImagePreview"
@update:open="(v) => !v && closeImagePreview()"
size="lg"
:title="previewImage?.filename"
:title="raw(previewImage?.filename)"
>
<div class="flex justify-center">
<img
@ -1018,7 +1018,7 @@
class="rounded-default border-border-default max-h-37.5 max-w-50 cursor-pointer border object-contain [transition:transform_0.2s_ease,box-shadow_0.2s_ease] hover:scale-102 hover:shadow-[0_4px_12px_color-mix(in_srgb,var(--color-black)_15%,transparent)]"
@click="openImagePreview(img)"
/>
<OTooltip :content="img.filename" />
<OTooltip :content="raw(img.filename)" />
</div>
</div>
<template v-for="(block, blockIndex) in message.blocks" :key="'fb-' + blockIndex">
@ -1292,7 +1292,7 @@
<RichTextInput
ref="chatInput"
v-model="inputMessage"
:placeholder="inputPlaceholder"
:placeholder="raw(inputPlaceholder)"
:disabled="isLoading"
:theme="store.state.theme"
:references="contextReferences"
@ -1391,7 +1391,7 @@ import {
computed,
onUnmounted,
} from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import { useRouter, useRoute } from "vue-router";
import { useTypewriterPlaceholder } from "@/components/ai-assistant/welcome/useTypewriterPlaceholder";
import hljs from "highlight.js";
@ -1652,7 +1652,7 @@ export default defineComponent({
const pendingConfirmation = ref<{
tool: string;
args: Record<string, any>;
message: string;
message: I18nText;
navAction?: NavigationAction;
} | null>(null);
@ -1695,7 +1695,7 @@ export default defineComponent({
// Active tool call state - for showing tool progress outside message box
const activeToolCall = ref<{
tool: string;
message: string;
message: I18nText;
context: Record<string, any>;
call_id?: string;
} | null>(null);
@ -2035,7 +2035,9 @@ export default defineComponent({
}
}
// Keep partial content but indicate it was cancelled
lastMessage.content += "\n\n_[Response stopped by user]_";
lastMessage.content = raw(
lastMessage.content + "\n\n_[" + t("aiAssistant.responseStoppedByUser") + "]_",
);
}
}
}
@ -2053,10 +2055,10 @@ export default defineComponent({
// Tools ran before any text, so place them ahead of it.
lastMessage.contentBlocks.unshift(...pendingToolCalls.value);
} else {
const stoppedNote = "_[Response stopped by user]_";
const stoppedNote = `_[${t("aiAssistant.responseStoppedByUser")}]_`;
chatMessages.value.push({
role: "assistant",
content: stoppedNote,
content: raw(stoppedNote),
contentBlocks: [...pendingToolCalls.value, { type: "text", text: stoppedNote }],
});
}
@ -2217,7 +2219,7 @@ export default defineComponent({
chatMessages.value = [
{
role: "assistant",
content: "Error: Unable to connect to backend",
content: t("aiAssistant.backendConnectionError"),
},
];
console.error("Error fetching initial message:", error);
@ -2445,7 +2447,7 @@ export default defineComponent({
} else {
msgs.push({
role: "assistant",
content: "",
content: raw(""),
contentBlocks: [...pendingToolCalls.value, confirmBlock],
});
pendingToolCalls.value = [];
@ -2533,7 +2535,7 @@ export default defineComponent({
if (!lastMessage || lastMessage.role !== "assistant") {
msgs.push({
role: "assistant",
content: errorMessage,
content: raw(errorMessage),
contentBlocks: [
...pendingToolCalls.value,
{ type: "text", text: errorMessage },
@ -2543,9 +2545,9 @@ export default defineComponent({
} else {
// Append error to existing message
if (lastMessage.content) {
lastMessage.content += "\n\n" + errorMessage;
lastMessage.content = raw(lastMessage.content + "\n\n" + errorMessage);
} else {
lastMessage.content = errorMessage;
lastMessage.content = raw(errorMessage);
}
if (!lastMessage.contentBlocks) {
lastMessage.contentBlocks = [];
@ -2607,7 +2609,7 @@ export default defineComponent({
} else {
msgs.push({
role: "assistant",
content: "",
content: raw(""),
contentBlocks: [...pendingToolCalls.value],
});
}
@ -2772,7 +2774,7 @@ export default defineComponent({
} else {
msgs.push({
role: "assistant",
content: "",
content: raw(""),
contentBlocks: [...pendingToolCalls.value, confirmBlock],
});
pendingToolCalls.value = [];
@ -2835,7 +2837,7 @@ export default defineComponent({
} else {
msgs.push({
role: "assistant",
content: "",
content: raw(""),
contentBlocks: [...pendingToolCalls.value, errorBlock],
});
pendingToolCalls.value = [];
@ -2916,7 +2918,7 @@ export default defineComponent({
// Create new assistant message with pending tool calls + text
msgs.push({
role: "assistant",
content: streamingMsg,
content: raw(streamingMsg),
contentBlocks: [
...pendingToolCalls.value,
{ type: "text", text: textSegment },
@ -2927,7 +2929,7 @@ export default defineComponent({
await throttledSaveCtx(true);
} else {
// Update existing assistant message's total content
lastMessage.content = streamingMsg;
lastMessage.content = raw(streamingMsg);
// Update or add text block in contentBlocks
if (!lastMessage.contentBlocks) {
@ -3053,7 +3055,7 @@ export default defineComponent({
if (!lastMessage || lastMessage.role !== "assistant") {
msgs.push({
role: "assistant",
content: errorMessage,
content: raw(errorMessage),
contentBlocks: [
...pendingToolCalls.value,
{ type: "text", text: errorMessage },
@ -3062,9 +3064,9 @@ export default defineComponent({
pendingToolCalls.value = [];
} else {
if (lastMessage.content) {
lastMessage.content += "\n\n" + errorMessage;
lastMessage.content = raw(lastMessage.content + "\n\n" + errorMessage);
} else {
lastMessage.content = errorMessage;
lastMessage.content = raw(errorMessage);
}
if (!lastMessage.contentBlocks) {
lastMessage.contentBlocks = [];
@ -3215,7 +3217,7 @@ export default defineComponent({
} else {
msgs.push({
role: "assistant",
content: "",
content: raw(""),
contentBlocks: [...pendingToolCalls.value, errorBlock],
});
pendingToolCalls.value = [];
@ -3280,7 +3282,7 @@ export default defineComponent({
if (!lastMessage || lastMessage.role !== "assistant") {
msgs.push({
role: "assistant",
content: streamingMsg,
content: raw(streamingMsg),
contentBlocks: [
...pendingToolCalls.value,
{ type: "text", text: textSegment },
@ -3289,7 +3291,7 @@ export default defineComponent({
pendingToolCalls.value = [];
await throttledSaveCtx(true);
} else {
lastMessage.content = streamingMsg;
lastMessage.content = raw(streamingMsg);
if (!lastMessage.contentBlocks) {
lastMessage.contentBlocks = [];
@ -3481,7 +3483,7 @@ export default defineComponent({
{
id: "aiChatClose",
key: "escape",
description: "Close AI chat",
description: t("shortcuts.actions.aiChatClose"),
// Escape must close the chat even while typing a message in its input.
allowInInput: true,
handler: () => {
@ -3496,7 +3498,7 @@ export default defineComponent({
id: "aiChatExpand",
key: "ctrl+b",
keyForMac: "meta+b",
description: "Expand/collapse AI chat",
description: t("shortcuts.actions.aiChatExpand"),
handler: toggleExpand,
},
]);
@ -3762,10 +3764,14 @@ export default defineComponent({
target.functionContent = vrlFunction;
}
const streamLabel =
{ logs: t("common.logs"), metrics: t("common.metrics"), traces: t("common.traces") }[
streamType as string
] ?? raw(streamType.charAt(0).toUpperCase() + streamType.slice(1));
return {
resource_type: streamType,
action: "load_query",
label: `View in ${streamType.charAt(0).toUpperCase() + streamType.slice(1)}`,
label: t("aiAssistant.viewInTarget", { target: streamLabel }),
target,
};
}
@ -3866,7 +3872,9 @@ export default defineComponent({
return {
resource_type: resourceType,
action: "navigate_direct",
label: `View ${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)}`,
label: t("aiAssistant.viewTarget", {
target: resourceType.charAt(0).toUpperCase() + resourceType.slice(1),
}),
target,
};
};
@ -3989,22 +3997,22 @@ export default defineComponent({
setTimeout(async () => {
try {
// Add success message AFTER navigation completes
const successMessage = `Successfully navigated to ${pageName}`;
const successMessage = t("aiAssistant.navigatedTo", { page: pageName });
let lastMessage = chatMessages.value[chatMessages.value.length - 1];
if (!lastMessage || lastMessage.role !== "assistant") {
// Create new assistant message
chatMessages.value.push({
role: "assistant",
content: successMessage,
content: raw(successMessage),
contentBlocks: [{ type: "text", text: successMessage }],
});
} else {
// Append to existing assistant message
if (lastMessage.content) {
lastMessage.content += "\n\n" + successMessage;
lastMessage.content = raw(lastMessage.content + "\n\n" + successMessage);
} else {
lastMessage.content = successMessage;
lastMessage.content = raw(successMessage);
}
if (!lastMessage.contentBlocks) {
lastMessage.contentBlocks = [];
@ -4161,7 +4169,7 @@ export default defineComponent({
// But we'll use backendMessage for the API call
chatMessages.value.push({
role: "user",
content: backendMessage, // Use backend message with full context
content: raw(backendMessage), // Use backend message with full context
...(hasImages && { images: messagesToSend }),
});
inputMessage.value = "";
@ -4280,7 +4288,7 @@ export default defineComponent({
}
chatMessages.value.push({
role: "assistant",
content: errorMessage,
content: raw(errorMessage),
});
await saveToHistory(); // Save after error
}
@ -5639,6 +5647,7 @@ export default defineComponent({
});
return {
raw,
inputMessage,
chatMessages,
isLoading,

View File

@ -88,7 +88,7 @@
@click="handleConfirm"
@focus="handleYesFocus"
@blur="handleYesBlur"
>{{ confirmLabel }}</OButton
>{{ resolvedConfirmLabel }}</OButton
>
<OButton
ref="noButtonRef"
@ -104,7 +104,7 @@
@click="handleCancel"
@focus="handleNoFocus"
@blur="handleNoBlur"
>{{ cancelLabel }}</OButton
>{{ resolvedCancelLabel }}</OButton
>
</template>
</div>
@ -114,31 +114,34 @@
<script setup lang="ts">
import { ref, watch, nextTick, computed, onMounted, onUnmounted } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
interface ConfirmationData {
tool?: string;
args?: Record<string, any>;
message?: string;
message?: I18nText;
}
interface Props {
visible: boolean;
confirmation: ConfirmationData | null;
confirmLabel?: string;
cancelLabel?: string;
confirmLabel?: I18nText;
cancelLabel?: I18nText;
}
const { t } = useI18nTyped();
const props = withDefaults(defineProps<Props>(), {
confirmLabel: "Yes",
cancelLabel: "No",
confirmation: null,
});
// Render-time defaults so the labels stay locale-reactive; a withDefaults
// literal would freeze the English text.
const resolvedConfirmLabel = computed(() => props.confirmLabel ?? t("common.yes"));
const resolvedCancelLabel = computed(() => props.cancelLabel ?? t("common.no"));
const emit = defineEmits<{
confirm: [];
cancel: [];

View File

@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
import { useVirtualizer } from "@tanstack/vue-virtual";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import OButton from "@/lib/core/Button/OButton.vue";
@ -39,7 +39,7 @@ import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
import { copyToClipboard } from "@/utils/clipboard";
interface OrgOption {
label: string;
label: I18nText;
identifier: string;
[key: string]: any;
}

View File

@ -140,7 +140,7 @@
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import { useTheme } from "@/composables/useTheme";
import CodeQueryEditor from "@/components/CodeQueryEditor.vue";
import OButton from "@/lib/core/Button/OButton.vue";
@ -177,9 +177,9 @@ interface Props {
editorHeight?: string;
hideNlToggle?: boolean; // Hide floating AI icon (for pages that don't want AI)
disableAi?: boolean; // Disable AI send (e.g. no stream selected)
disableAiReason?: string; // Tooltip reason when AI is disabled
aiPlaceholder?: string; // Custom placeholder for AI input (default: 'search.askAIPlaceholder')
aiTooltip?: string; // Custom tooltip for AI send button (default: 'search.enterPrompt')
disableAiReason?: I18nText; // Tooltip reason when AI is disabled
aiPlaceholder?: I18nText; // Custom placeholder for AI input (default: 'search.askAIPlaceholder')
aiTooltip?: I18nText; // Custom tooltip for AI send button (default: 'search.enterPrompt')
hasExpandButton?: boolean; // Reserve right padding so AI bar close btn doesn't overlap the expand btn
// Testing
@ -199,7 +199,7 @@ const props = withDefaults(defineProps<Props>(), {
editorHeight: "200px",
hideNlToggle: false,
disableAi: false,
disableAiReason: "",
disableAiReason: raw(""),
hasExpandButton: false,
dataTestPrefix: "query-editor",
});
@ -368,7 +368,7 @@ const handleAIGenerate = async () => {
currentAbortController.value = new AbortController();
// Track user message for chat history
chatMessages.value.push({ role: "user", content: userInput });
chatMessages.value.push({ role: "user", content: raw(userInput) });
// Call the CodeQueryEditor's handleGenerateSQL method with abort + session
if (editorRef.value && typeof editorRef.value.handleGenerateSQL === "function") {

View File

@ -86,7 +86,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<div v-else-if="error" class="p-4">
<OBanner variant="error" icon="error" :content="error" />
<OBanner variant="error" icon="error" :content="raw(error)" />
</div>
<!-- EXPLAIN ANALYZE view -->
@ -180,7 +180,7 @@ import OIcon from "@/lib/core/Icon/OIcon.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import { defineComponent, ref, computed, watch } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import streamingSearch from "@/services/streaming_search";
import { useSearchStream } from "@/composables/useLogs/useSearchStream";
@ -596,6 +596,7 @@ export default defineComponent({
);
return {
raw,
t,
showDialog,
loading,

View File

@ -14,7 +14,7 @@
class="rich-text-input text-text-body relative max-h-75 min-h-10 overflow-y-auto text-sm leading-[1.6] break-words whitespace-pre-wrap outline-none"
:class="disabled ? 'cursor-not-allowed' : ''"
contenteditable="true"
:data-placeholder="placeholder"
:data-placeholder="resolvedPlaceholder"
@input="handleInput"
@keydown="handleKeyDown"
@paste="handlePaste"
@ -42,7 +42,8 @@
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, watch, nextTick, PropType } from "vue";
import { useI18nTyped, type I18nText } from "@/types/i18n";
import { computed, defineComponent, ref, onMounted, watch, nextTick, PropType } from "vue";
export interface ReferenceChip {
id: string;
@ -63,8 +64,9 @@ export default defineComponent({
default: "",
},
placeholder: {
type: String,
default: "Write your prompt",
type: String as unknown as PropType<I18nText>,
// Resolved in setup so the fallback is translated, not frozen at load.
default: undefined,
},
disabled: {
type: Boolean,
@ -87,6 +89,8 @@ export default defineComponent({
},
emits: ["update:modelValue", "keydown", "submit", "focus", "blur", "update:references"],
setup(props, { emit }) {
const { t } = useI18nTyped();
const resolvedPlaceholder = computed(() => props.placeholder ?? t("common.writeYourPrompt"));
const editableDiv = ref<HTMLDivElement | null>(null);
const isFocused = ref(false);
const localReferences = ref<ReferenceChip[]>([...props.references]);
@ -614,6 +618,7 @@ export default defineComponent({
});
return {
resolvedPlaceholder,
editableDiv,
isFocused,
handleInput,

View File

@ -29,14 +29,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="size-5!"
/>
</Transition>
<OTooltip side="top" align="center" :content="tooltipText" />
<OTooltip side="top" align="center" :content="raw(tooltipText)" />
</OButton>
</template>
<script lang="ts">
import { ref, watch, onMounted, computed, defineComponent } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
@ -112,6 +112,7 @@ export default defineComponent({
};
return {
raw,
store,
darkMode,
tooltipText,

View File

@ -128,7 +128,7 @@ import { ref, computed, onMounted, watch } from "vue";
import { useStore } from "vuex";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
const store = useStore();
const { t } = useI18nTyped();
@ -149,7 +149,7 @@ interface WebinarData {
id: number;
documentId: string;
tag: string;
title: string;
title: I18nText;
date: string;
primaryButton: PrimaryButton;
}

View File

@ -409,7 +409,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup lang="ts">
import { ref, nextTick, onMounted, watch } from "vue";
import { computed } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import { useRouter } from "vue-router";
import {
getUUID,
@ -484,11 +484,11 @@ const formData = ref(defaultActionScript);
const actionTypes = [
{
label: "Scheduled",
label: t("home.scheduledAlert"),
value: "scheduled",
},
{
label: "Real Time",
label: t("actions.realTime"),
value: "service",
},
];
@ -502,11 +502,11 @@ const dialog = ref({
const frequencyTabs = [
{
label: "Cron Job",
label: t("actions.cronJob"),
value: "repeat",
},
{
label: "Once",
label: t("reports.frequencyOnce"),
value: "once",
},
];
@ -980,7 +980,7 @@ const handleActionScript = async () => {
}
};
const filteredServiceAccounts: Ref<{ label: string; value: string }[]> = ref([]);
const filteredServiceAccounts: Ref<{ label: I18nText; value: string }[]> = ref([]);
const isFetchingServiceAccounts = ref(false);
const serviceAccountsOptions: any[] = [];

View File

@ -1,5 +1,5 @@
import { computed } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nKey } from "@/types/i18n";
/**
* Builds a time-of-day greeting using the user's local timezone.
@ -27,7 +27,10 @@ export function useGreeting(email: () => string | undefined) {
});
const greeting = computed(() => {
const key = `aiAssistant.greeting.${period.value}`;
// Annotated so the built key is checked against I18nKey: `period` is a
// literal union, so TS expands this to the four real keys and a rename or
// deletion of any of them fails the build instead of rendering the path.
const key: I18nKey = `aiAssistant.greeting.${period.value}`;
const phrase = t(key);
return displayName.value ? `${phrase}, ${displayName.value}` : phrase;
});

View File

@ -78,7 +78,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="o2-input mb-4">
<!-- eslint-disable vue/no-bare-strings-in-template -- example URL format, not translatable content -->
<OFormInput
placeholder="https://api.example.com/mcp/"
:placeholder="raw('https://api.example.com/mcp/')"
data-test="ai-toolset-mcp-url"
name="mcp.url"
:label="t('aiToolset.mcpUrl')"
@ -154,7 +154,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="o2-input mb-4">
<!-- eslint-disable vue/no-bare-strings-in-template -- example CLI command name, not translatable content -->
<OFormInput
placeholder="kubectl"
:placeholder="raw('kubectl')"
data-test="ai-toolset-cli-command"
name="cli.command"
:label="t('aiToolset.cliCommand')"
@ -166,7 +166,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="o2-input mb-4">
<!-- eslint-disable vue/no-bare-strings-in-template -- example CLI subcommand names, not translatable content -->
<OFormInput
placeholder="get, describe, logs"
:placeholder="raw('get, describe, logs')"
name="cli.allowed_subcommands_raw"
:label="t('aiToolset.allowedSubcommands')"
:helpText="t('aiToolset.subcommandsHint')"
@ -246,7 +246,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OFormInput
:name="`cli.credFiles[${idx}].key`"
:label="t('aiToolset.credEnvVar')"
helpText="e.g. KUBECONFIG"
:helpText="raw('e.g. KUBECONFIG')"
class="o2-input w-48"
/>
<OButton
@ -329,7 +329,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { defineAsyncComponent, defineComponent, ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import aiToolsetsService from "@/services/ai_toolsets";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
@ -378,9 +378,9 @@ export default defineComponent({
const isEditing = ref(false);
const kindOptions = [
{ label: "MCP Server", value: "mcp" },
{ label: "CLI Tool", value: "cli" },
{ label: "Skill", value: "skill" },
{ label: t("aiToolset.kindMcp"), value: "mcp" },
{ label: t("aiToolset.kindCli"), value: "cli" },
{ label: t("aiToolset.kindSkill"), value: "skill" },
];
// -----------------------------------------------------------------------
@ -445,7 +445,7 @@ export default defineComponent({
try {
if (isEditing.value && editingId.value) {
await aiToolsetsService.update(org, editingId.value, {
description: value.description || undefined,
description: value.description ? raw(value.description) : undefined,
data,
});
toast({
@ -456,7 +456,7 @@ export default defineComponent({
await aiToolsetsService.create(org, {
name: value.name,
kind: value.kind as ToolsetKind,
description: value.description || undefined,
description: value.description ? raw(value.description) : undefined,
data,
});
toast({
@ -654,6 +654,7 @@ export default defineComponent({
});
return {
raw,
t,
// Exposed for the theme-aware footer background (`store.state.theme`).
store,

View File

@ -46,7 +46,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</span>
<span v-else class="truncate" :title="anomalyConfig.name">
{{ anomalyConfig.name }}
<OTooltip v-if="anomalyConfig.name?.length > 24" :content="anomalyConfig.name" />
<OTooltip
v-if="anomalyConfig.name?.length > 24"
:content="raw(anomalyConfig.name)"
/>
</span>
</template>
</template>
@ -525,6 +528,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script lang="ts">
import { raw } from "@/types/i18n";
import { defineComponent, computed, watch, provide } from "vue";
import type { SelectOption } from "@/lib/forms/Select/OSelect.types";
import OButton from "@/lib/core/Button/OButton.vue";
@ -691,6 +695,7 @@ export default defineComponent({
};
return {
raw,
...alertForm,
isAnomalyDetectionEnabled,
alertTypeOptions,

View File

@ -440,7 +440,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts" setup>
import { ref, computed, onBeforeMount, onActivated, watch } from "vue";
import type { PropType } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import destinationService from "@/services/alert_destination";
import { useStore } from "vuex";
import OButton from "@/lib/core/Button/OButton.vue";
@ -519,7 +519,7 @@ const apiHeaders = form.useStore(
const isUpdatingDestination = ref(false);
const isLoadingActions = ref(false);
const router = useRouter();
const actionOptions = ref<{ value: string; label: string; type: string }[]>([]);
const actionOptions = ref<{ value: string; label: I18nText; type: string }[]>([]);
const { getAllActions } = useActions();
@ -895,17 +895,17 @@ const prebuiltTemplateOptions = computed(() => {
return template.type !== "email";
});
const options: { label: string; value: string }[] = [];
const options: { label: I18nText; value: string }[] = [];
if (defaultPrebuiltTemplateName.value) {
const defaultLabel = t("alert_destinations.templateDefaultOption", {
name: defaultPrebuiltTemplateName.value,
});
options.push({ label: defaultLabel, value: defaultPrebuiltTemplateName.value });
options.push({ label: raw(defaultLabel), value: defaultPrebuiltTemplateName.value });
}
matching.forEach((template) => {
options.push({ label: template.name, value: template.name });
options.push({ label: raw(template.name), value: template.name });
});
return options;

View File

@ -433,13 +433,15 @@ describe("AlertHistory.vue", () => {
expect(r.variant, s).toBe("success-soft");
expect(r.icon, s).toBe("check-circle-outline");
}
expect(resolveBadge("alertState", "ok").label).toBe("Ok");
// "Ok" moved from a hardcoded `label` to a translatable `labelKey`,
// which OTag resolves via t() (label precedence: prop → labelKey → label).
expect(resolveBadge("alertState", "ok").labelKey).toBe("components.badge.alertState.ok");
});
it("condition_not_satisfied → green 'Ok' (matches the histogram count)", () => {
const r = resolveBadge("alertState", "condition_not_satisfied");
expect(r.variant).toBe("success-soft");
expect(r.label).toBe("Ok");
expect(r.labelKey).toBe("components.badge.alertState.ok");
expect(r.icon).toBe("check-circle-outline");
});

View File

@ -113,7 +113,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
unit="us"
mode="absolute"
:timezone="store.state.timezone"
empty-label="—"
:empty-label="raw('')"
/>
</template>
@ -123,7 +123,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
unit="us"
mode="absolute"
:timezone="store.state.timezone"
empty-label="—"
:empty-label="raw('')"
/>
</template>
@ -133,7 +133,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
unit="us"
mode="absolute"
:timezone="store.state.timezone"
empty-label="—"
:empty-label="raw('')"
/>
</template>
@ -449,7 +449,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ref, onMounted, watch } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { formatDate } from "@/utils/date";
import DateTime from "@/components/DateTime.vue";
import OTable from "@/lib/core/Table/OTable.vue";

View File

@ -135,21 +135,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup lang="ts">
import { ref, computed } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
const { t } = useI18nTyped();
const props = withDefaults(
defineProps<{
history: Array<{ status: string; timestamp: number }>;
// Legend/tooltip wording. Defaults are alert-centric ("Firing"/"Ok");
// workflows pass "Failed"/"Success".
firingLabel?: string;
okLabel?: string;
}>(),
{ firingLabel: "Firing", okLabel: "Ok" },
);
const { firingLabel, okLabel } = props;
const props = defineProps<{
history: Array<{ status: string; timestamp: number }>;
// Legend/tooltip wording. Defaults are alert-centric ("Firing"/"Ok");
// workflows pass "Failed"/"Success".
firingLabel?: I18nText;
okLabel?: I18nText;
}>();
// Resolved here rather than as `withDefaults` defaults: a default is evaluated
// once at module scope, which would freeze the wording in whatever locale
// happened to be active when the module first loaded.
const firingLabel = computed(() => props.firingLabel ?? t("alerts.historyTimeline.firing"));
const okLabel = computed(() => props.okLabel ?? t("alerts.historyTimeline.ok"));
const hoveredIndex = ref<number | null>(null);
@ -167,10 +169,10 @@ function isOk(s: string) {
function normalizeStatus(s: string): string {
const v = s?.toLowerCase();
if (isFiring(v)) return firingLabel;
if (isOk(v)) return okLabel;
if (v === "skipped") return "Skipped";
return s?.replace(/_/g, " ") ?? "Unknown";
if (isFiring(v)) return firingLabel.value;
if (isOk(v)) return okLabel.value;
if (v === "skipped") return t("alerts.historyTimeline.skipped");
return s?.replace(/_/g, " ") ?? t("alerts.historyTimeline.unknown");
}
function blockColor(status: string): string {

View File

@ -273,7 +273,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
unit="iso"
mode="absolute"
:timezone="store.state.timezone"
empty-label="—"
:empty-label="raw('')"
/>
</template>
@ -709,7 +709,7 @@ import { useRouter } from "vue-router";
import useStreams from "@/composables/useStreams";
import { convertUnixToDateFormat as convertUnixToFormat } from "@/utils/date";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { debounce } from "lodash-es";
import alertsService from "@/services/alerts";
import destinationService from "@/services/alert_destination";
@ -2835,6 +2835,7 @@ export default defineComponent({
]);
return {
raw,
t,
store,
router,

View File

@ -82,13 +82,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import OSelect from "@/lib/forms/Select/OSelect.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
interface Option {
label: string;
label: I18nText;
value: string;
}
// Option lists may be plain strings (destinations = `getFormattedDestinations`
@ -152,12 +152,12 @@ watch(propsCombined, (next) => {
// Normalize a raw option (string or {label,value}) to a { name, label } pair.
// Returns null for anything unusable (see the isFilled note above) so the caller
// can drop it rather than render a `dest:undefined` row or throw.
const norm = (o: RawOption): { name: string; label: string } | null => {
const norm = (o: RawOption): { name: string; label: I18nText } | null => {
if (!isFilled(o)) return null;
if (typeof o === "string") return { name: o, label: o };
if (typeof o === "string") return { name: o, label: raw(o) };
if (!isFilled((o as Option).value)) return null;
const value = String((o as Option).value);
return { name: value, label: (o as Option).label ?? value };
return { name: value, label: (o as Option).label ?? raw(value) };
};
const toTagged = (list: RawOption[] | undefined, tag: string) =>

View File

@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="custom-confirm-dialog"
v-model:open="isVisible"
size="sm"
:title="title"
:title="resolvedTitle"
persistent
:show-close="false"
:secondary-button-label="t('common.cancel')"
@ -34,9 +34,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { computed, defineComponent, ref, watch, type PropType } from "vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
export default defineComponent({
name: "CustomConfirmDialog",
@ -47,12 +47,14 @@ export default defineComponent({
default: false,
},
title: {
type: String,
default: "Confirm Action",
type: String as unknown as PropType<I18nText>,
// Resolved in setup, not here: a literal default would ship untranslated,
// and t() at module scope would freeze the copy at page-load locale.
default: undefined,
},
message: {
type: String,
default: "",
type: String as unknown as PropType<I18nText>,
default: raw(""),
},
},
emits: ["update:modelValue", "confirm", "cancel"],
@ -76,12 +78,16 @@ export default defineComponent({
emit("cancel");
};
const resolvedTitle = computed(() => props.title ?? t("common.confirmAction"));
const onConfirm = () => {
isVisible.value = false;
emit("confirm");
};
return {
raw,
resolvedTitle,
isVisible,
onCancel,
onConfirm,

View File

@ -17,7 +17,7 @@ limitations under the License.
<ODialog
v-model:open="isOpen"
size="md"
:title="`${t('alerts.destinationPreview')} - ${getDestinationTypeName(type)}`"
:title="raw(`${t('alerts.destinationPreview')} - ${getDestinationTypeName(type)}`)"
data-test="destination-preview-dialog"
>
<div data-test="destination-preview-card" class="w-full">
@ -398,7 +398,7 @@ limitations under the License.
<script lang="ts" setup>
import { computed } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import OButton from "@/lib/core/Button/OButton.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import { copyToClipboard } from "@/utils/clipboard";
@ -447,8 +447,8 @@ const getDestinationTypeName = (type: string): string => {
// Copy template to clipboard
const copyTemplate = () => {
copyToClipboard(props.templateContent, t, {
successMessage: "Template copied to clipboard",
errorMessage: "Failed to copy template",
successMessage: t("alerts.previewCopyTemplateSuccess"),
errorMessage: t("alerts.previewCopyTemplateError"),
timeout: 2000,
});
};

View File

@ -138,7 +138,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:placeholder="t('alerts.column')"
:creatable="props.enableNewValueMode"
:error="!!fieldErrors[`${field.uuid}-column`]"
:error-message="fieldErrors[`${field.uuid}-column`] || ''"
:error-message="raw(fieldErrors[`${field.uuid}-column`] || '')"
data-test="alert-conditions-select-column"
@create="
(val: string) => {
@ -162,7 +162,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:options="triggerOperators"
class="min-w-30 py-2"
:error="!!fieldErrors[`${field.uuid}-operator`]"
:error-message="fieldErrors[`${field.uuid}-operator`] || ''"
:error-message="raw(fieldErrors[`${field.uuid}-operator`] || '')"
data-test="alert-conditions-operator-select"
@update:model-value="
(v: any) => {
@ -180,7 +180,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:placeholder="t('common.value')"
class="min-w-37.5 py-2"
:error="!!fieldErrors[`${field.uuid}-value`]"
:error-message="fieldErrors[`${field.uuid}-value`] || ''"
:error-message="raw(fieldErrors[`${field.uuid}-value`] || '')"
data-test="alert-conditions-value-input"
@update:model-value="
(v: any) => {
@ -224,7 +224,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts" setup>
import { ref, computed, reactive, inject } from "vue";
import type { PropType, Ref } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OInput from "@/lib/forms/Input/OInput.vue";

View File

@ -113,8 +113,8 @@ const props = defineProps({
required: true,
},
label: {
type: String,
default: "",
type: String as unknown as PropType<I18nText>,
default: raw(""),
required: true,
},
depth: {
@ -156,8 +156,8 @@ const props = defineProps({
},
});
import { ref, computed, watch, inject } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { ref, computed, watch, inject, type PropType } from "vue";
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import OIcon from "@/lib/core/Icon/OIcon.vue";

View File

@ -114,7 +114,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OInput
data-test="alert-import-name-input"
:model-value="userSelectedAlertName[index] || ''"
:label="t('alerts.name') + ' *'"
:label="raw(t('alerts.name') + ' *')"
:error="!userSelectedAlertName[index]?.toString().trim()"
:error-message="t('alerts.validation.fieldRequired')"
@update:model-value="
@ -139,7 +139,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="alert-import-stream-name-input"
:model-value="userSelectedStreamName[index] || ''"
:options="streamList"
:label="t('alerts.stream_name') + ' *'"
:label="raw(t('alerts.stream_name') + ' *')"
searchable
:error="!userSelectedStreamName[index]"
:error-message="t('alerts.validation.fieldRequired')"
@ -192,7 +192,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="alert-import-stream-type-input"
:model-value="userSelectedStreamType[index] || ''"
:options="streamTypes"
:label="t('alerts.streamType') + ' *'"
:label="raw(t('alerts.streamType') + ' *')"
class="w-75!"
:error="!userSelectedStreamType[index]"
:error-message="t('alerts.validation.fieldRequired')"
@ -295,7 +295,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, onMounted, computed, watch } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import alertsService from "../../services/alerts";
@ -347,10 +347,10 @@ export default defineComponent({
setup(props, { emit }) {
type ErrorMessage = {
field: string;
message: string;
message: I18nText;
};
type alertCreator = {
message: string;
message: I18nText;
success: boolean;
}[];
@ -400,7 +400,7 @@ export default defineComponent({
const organizationDataList = computed(() => {
return store.state.organizations.map((org: any) => {
return {
label: org.identifier,
label: raw(org.identifier),
value: org.identifier,
disabled:
!org.identifier || org.identifier !== store.state.selectedOrganization.identifier,
@ -568,13 +568,17 @@ export default defineComponent({
};
await anomalyDetectionService.create(org, payload);
alertCreators.value.push({
message: `Anomaly Detection - ${index}: "${jsonObj.name}" imported successfully`,
message: t("alerts.import.anomalyImportSuccess", { index, name: jsonObj.name }),
success: true,
});
return true;
} catch (e: any) {
alertCreators.value.push({
message: `Anomaly Detection - ${index}: "${jsonObj.name}" import failed — ${e?.response?.data?.message || "Unknown Error"}`,
message: t("alerts.import.anomalyImportFailed", {
index,
name: jsonObj.name,
reason: e?.response?.data?.message || t("alerts.import.unknownError"),
}),
success: false,
});
return false;
@ -608,12 +612,12 @@ export default defineComponent({
};
const validateAlertInputs = async (input: any, index: number) => {
let alertErrors: (string | { message: string; field: string })[] = [];
let alertErrors: (string | ErrorMessage)[] = [];
// 1. Validate 'name' field
if (!input.name || typeof input.name !== "string" || input.name.trim() === "") {
alertErrors.push({
message: `Alert - ${index}: Name is mandatory and should be a valid string.`,
message: t("alerts.import.nameRequired", { index }),
field: "alert_name",
});
}
@ -626,7 +630,10 @@ export default defineComponent({
input.org_id != store.state.selectedOrganization.identifier
) {
alertErrors.push({
message: `Alert - ${index}: Organization Id is mandatory, should exist in organization list and should be equal to ${store.state.selectedOrganization.identifier}.`,
message: t("alerts.import.orgIdInvalid", {
index,
orgId: store.state.selectedOrganization.identifier,
}),
field: "org_id",
});
}
@ -635,7 +642,7 @@ export default defineComponent({
const validStreamTypes = ["logs", "metrics", "traces"];
if (!input.stream_type || !validStreamTypes.includes(input.stream_type)) {
alertErrors.push({
message: `Alert - ${index}: Stream Type is mandatory and should be one of: 'logs', 'metrics', 'traces'.`,
message: t("alerts.import.streamTypeInvalid", { index }),
field: "stream_type",
});
}
@ -658,7 +665,7 @@ export default defineComponent({
!streamList.value.includes(input.stream_name)
) {
alertErrors.push({
message: `Alert - ${index}: Stream Name is mandatory, should exist in the stream list and should be a valid string.`,
message: t("alerts.import.streamNameInvalid", { index }),
field: "stream_name",
});
}
@ -895,17 +902,17 @@ export default defineComponent({
input.destinations.length === 0
) {
alertErrors.push({
message: `Alert - ${index}: Destinations are required and should be an array.`,
message: t("alerts.import.destinationsRequired", { index }),
field: "destination_name",
});
}
if (typeof input.enabled !== "boolean") {
alertErrors.push(`Alert - ${index}: Enabled should be Boolean.`);
alertErrors.push(t("alerts.import.enabledBoolean", { index }));
}
if (input.tz_offset && (typeof input.tz_offset !== "number" || input.tz_offset < 0)) {
alertErrors.push(`Alert - ${index}: Timezone offset should be a number.`);
alertErrors.push(t("alerts.import.tzOffsetNumber", { index }));
}
if (
@ -914,7 +921,7 @@ export default defineComponent({
input.trigger_condition.timezone === ""
) {
alertErrors.push({
message: `Alert - ${index}: Timezone is required when frequency type is 'cron'.`,
message: t("alerts.import.timezoneRequiredForCron", { index }),
field: "timezone",
});
}
@ -922,7 +929,7 @@ export default defineComponent({
input.destinations.forEach((destination: any) => {
if (!checkDestinationInList(props.destinations, destination)) {
alertErrors.push({
message: `Alert - ${index}: "${destination}" destination does not exist`,
message: t("alerts.import.destinationNotExist", { index, destination }),
field: "destination_name",
});
}
@ -1006,7 +1013,7 @@ export default defineComponent({
// Success
alertCreators.value.push({
message: `Alert - ${index}: "${input.name}" created successfully \nNote: please remove the created alert object ${input.name} from the json file`,
message: t("alerts.import.createSuccess", { index, name: input.name }),
success: true,
});
// Emit update after each successful creation
@ -1016,7 +1023,11 @@ export default defineComponent({
} catch (error: any) {
// Failure
alertCreators.value.push({
message: `Alert - ${index}: "${input.name}" creation failed --> \n Reason: ${error?.response?.data?.message || "Unknown Error"}`,
message: t("alerts.import.createFailed", {
index,
name: input.name,
reason: error?.response?.data?.message || t("alerts.import.unknownError"),
}),
success: false,
});
return false;
@ -1134,6 +1145,7 @@ export default defineComponent({
return {
t,
raw,
importJson,
router,
baseImportRef,

View File

@ -74,7 +74,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
updateDestinationName(val, index);
}
"
:label="t('alert_destinations.import.destinationName') + ' *'"
:label="raw(t('alert_destinations.import.destinationName') + ' *')"
class="showLabelOnTop"
tabindex="0"
/>
@ -97,7 +97,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
updateDestinationUrl(val, index);
}
"
:label="t('alert_destinations.import.destinationUrl') + ' *'"
:label="raw(t('alert_destinations.import.destinationUrl') + ' *')"
class="showLabelOnTop"
tabindex="0"
/>
@ -121,7 +121,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
"
:options="destinationTypes"
:label="t('alert_destinations.destination_type') + ' *'"
:label="raw(t('alert_destinations.destination_type') + ' *')"
class="showLabelOnTop no-case py-2"
/>
</div>
@ -144,7 +144,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
"
:options="destinationMethods"
:label="t('alert_destinations.import.destinationMethod') + ' *'"
:label="raw(t('alert_destinations.import.destinationMethod') + ' *')"
class="showLabelOnTop no-case py-2"
/>
</div>
@ -170,10 +170,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
"
:options="filteredTemplates"
:label="t('alert_destinations.import.templates') + ' *'"
:label="raw(t('alert_destinations.import.templates') + ' *')"
class="showLabelOnTop no-case py-2"
:error="!!templateErrors[index]"
:error-message="templateErrors[index]"
:error-message="raw(templateErrors[index])"
@search="filterTemplates"
/>
</div>
@ -197,7 +197,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
updateDestinationEmails(val, index);
}
"
:label="t('alert_destinations.import.emails') + ' *'"
:label="raw(t('alert_destinations.import.emails') + ' *')"
class="showLabelOnTop"
tabindex="0"
/>
@ -224,12 +224,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
"
:options="filteredActions"
:label="t('alert_destinations.import.actions') + ' *'"
:label="raw(t('alert_destinations.import.actions') + ' *')"
labelKey="label"
valueKey="value"
class="showLabelOnTop no-case w-75! py-2"
:error="!!actionErrors[index]"
:error-message="actionErrors[index]"
:error-message="raw(actionErrors[index])"
@search="filterActions"
/>
</div>
@ -297,7 +297,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, computed, reactive, onMounted } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import destinationService from "@/services/alert_destination";
@ -331,10 +331,10 @@ export default defineComponent({
setup(props, { emit }) {
type ErrorMessage = {
field: string;
message: string;
message: I18nText;
};
type destinationCreator = {
message: string;
message: I18nText;
success: boolean;
}[];
type destinationErrors = (ErrorMessage | string)[][];
@ -402,7 +402,7 @@ export default defineComponent({
userSelectedActionOptions.value = actionsData.list
.filter((action: any) => action.execution_details_type === "service")
.map((action: any) => ({
label: action.name,
label: raw(action.name),
value: action.id,
}));
filteredActions.value = userSelectedActionOptions.value;
@ -622,7 +622,7 @@ export default defineComponent({
value ? "" : t("alerts.validation.fieldRequired");
const validateDestinationInputs = async (input: any, index: number) => {
let destinationErrors: (string | { message: string; field: string })[] = [];
let destinationErrors: (string | ErrorMessage)[] = [];
// Validate name
if (!input.name || typeof input.name !== "string" || input.name.trim() === "") {
@ -831,6 +831,7 @@ export default defineComponent({
};
return {
raw,
t,
importJson,
router,

View File

@ -251,7 +251,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="import-semantic-groups-group-dialog"
v-model:open="showGroupDialog"
size="md"
:title="selectedGroup?.display"
:title="raw(selectedGroup?.display)"
:sub-title="t('common.idPrefix', { id: selectedGroup?.id })"
:primary-button-label="t('common.close')"
@click:primary="showGroupDialog = false"
@ -282,7 +282,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="import-semantic-groups-modification-dialog"
v-model:open="showModificationDialog"
size="lg"
:title="selectedModification?.proposed.display"
:title="raw(selectedModification?.proposed.display)"
:sub-title="t('correlation.importSemanticGroups.compareChanges')"
:primary-button-label="t('common.close')"
@click:primary="showModificationDialog = false"
@ -355,7 +355,7 @@ import BaseImport from "@/components/common/BaseImport.vue";
import alertsService from "@/services/alerts";
import OCheckbox from "@/lib/forms/Checkbox/OCheckbox.vue";
import { toast } from "@/lib/feedback/Toast/useToast";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
interface SemanticGroup {
id: string;

View File

@ -228,7 +228,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="import-semantic-groups-drawer-group-dialog"
v-model:open="showGroupDialog"
size="md"
:title="selectedGroup?.display"
:title="raw(selectedGroup?.display)"
:sub-title="t('common.idPrefix', { id: selectedGroup?.id })"
:primary-button-label="t('common.close')"
@click:primary="showGroupDialog = false"
@ -254,7 +254,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="import-semantic-groups-drawer-modification-dialog"
v-model:open="showModificationDialog"
size="lg"
:title="selectedModification?.proposed.display"
:title="raw(selectedModification?.proposed.display)"
:sub-title="t('correlation.compareChanges')"
:primary-button-label="t('common.close')"
@click:primary="showModificationDialog = false"
@ -320,7 +320,7 @@ import OCheckbox from "@/lib/forms/Checkbox/OCheckbox.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OCollapsible from "@/lib/core/Collapsible/OCollapsible.vue";
import { toast } from "@/lib/feedback/Toast/useToast";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
const { t } = useI18nTyped();

View File

@ -186,7 +186,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, computed } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import templateService from "@/services/alert_templates";
@ -218,10 +218,10 @@ export default defineComponent({
setup(props, { emit }) {
type ErrorMessage = {
field: string;
message: string;
message: I18nText;
};
type templateCreator = {
message: string;
message: I18nText;
success: boolean;
}[];
type templateErrors = (ErrorMessage | string)[][];
@ -369,17 +369,17 @@ export default defineComponent({
};
const validateTemplateInputs = async (input: any, index: number) => {
let templateErrors: (string | { message: string; field: string })[] = [];
let templateErrors: (string | ErrorMessage)[] = [];
// Validate name using the updated props.templates
if (!input.name || typeof input.name !== "string" || input.name.trim() === "") {
templateErrors.push({
message: `Template - ${index}: The "name" field is required and should be a valid string.`,
message: t("alert_templates.import.nameRequired", { index }),
field: "template_name",
});
} else if (props.templates.some((template: any) => template.name === input.name)) {
templateErrors.push({
message: `Template - ${index}: "${input.name}" already exists`,
message: t("alert_templates.import.nameExists", { index, name: input.name }),
field: "template_name",
});
}
@ -387,7 +387,7 @@ export default defineComponent({
// Validate type
if (!input.type || (input.type !== "email" && input.type !== "http")) {
templateErrors.push({
message: `Template - ${index}: The "type" field must be either "email" or "http"`,
message: t("alert_templates.import.typeInvalid", { index }),
field: "type",
});
}
@ -395,14 +395,14 @@ export default defineComponent({
// Validate body
if (!input.body || typeof input.body !== "string" || input.body.trim() === "") {
templateErrors.push({
message: `Template - ${index}: The "body" field is required and should be a valid JSON string.`,
message: t("alert_templates.import.bodyRequired", { index }),
field: "body",
});
} else {
const result = validateTemplateBody(input.body);
if (!result.valid) {
templateErrors.push({
message: `Template - ${index}: The "body" field should contain valid JSON. Placeholders like {value} for numbers and "{name}" for strings are supported.`,
message: t("alert_templates.import.bodyInvalidJson", { index }),
field: "body",
});
}
@ -412,7 +412,7 @@ export default defineComponent({
if (input.type === "email") {
if (!input.title || typeof input.title !== "string" || input.title.trim() === "") {
templateErrors.push({
message: `Template - ${index}: The "title" field is required for email type templates.`,
message: t("alert_templates.import.titleRequiredEmail", { index }),
field: "title",
});
}
@ -445,7 +445,7 @@ export default defineComponent({
});
tempalteCreators.value.push({
message: `Template - ${index}: "${input.name}" created successfully \nNote: please remove the created alert object ${input.name} from the json file `,
message: t("alert_templates.import.createSuccess", { index, name: input.name }),
success: true,
});
@ -455,7 +455,11 @@ export default defineComponent({
return true;
} catch (error: any) {
tempalteCreators.value.push({
message: `Template - ${index}: "${input.name}" creation failed --> \n Reason: ${error?.response?.data?.message || "Unknown Error"}`,
message: t("alert_templates.import.createFailed", {
index,
name: input.name,
reason: error?.response?.data?.message || t("alert_templates.import.unknownError"),
}),
success: false,
});
return false;

View File

@ -62,7 +62,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
type="correlationReason"
:value="row.correlation_reason"
/>
<OTooltip :content="getReasonTooltip(row.correlation_reason)" side="top" />
<OTooltip :content="raw(getReasonTooltip(row.correlation_reason))" side="top" />
</span>
</template>
</OTable>
@ -71,7 +71,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, PropType, computed } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { formatToReadable } from "@/utils/date";
import OTag from "@/lib/core/Badge/OTag.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
@ -168,6 +168,7 @@ export default defineComponent({
};
return {
raw,
t,
columns,
formatTimestamp,

View File

@ -1233,8 +1233,9 @@ describe("IncidentDetailDrawer.vue", () => {
});
it("should have translation for fired times with parameter", () => {
const translation = wrapper.vm.t("alerts.incidents.firedTimes", { count: 5 });
expect(translation).toBe("Fired 5 time(s)");
// Pipe plural: `count` in the named bag selects the branch AND fills {count}.
expect(wrapper.vm.t("alerts.incidents.firedTimes", { count: 5 })).toBe("Fired 5 times");
expect(wrapper.vm.t("alerts.incidents.firedTimes", { count: 1 })).toBe("Fired 1 time");
});
it("should have translation for refresh correlated data", () => {

View File

@ -45,7 +45,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ incidentDetails.title }}
<OTooltip
v-if="incidentDetails && (incidentDetails.title?.length ?? 0) > 35"
:content="incidentDetails.title"
:content="raw(incidentDetails.title)"
/>
</span>
</template>
@ -58,14 +58,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OTag type="incidentStatus" :value="incidentDetails.status" />
<OTooltip
:content="
t('alerts.incidents.status') + ': ' + getStatusLabel(incidentDetails.status)
raw(t('alerts.incidents.status') + ': ' + getStatusLabel(incidentDetails.status))
"
/>
</span>
<span class="inline-flex cursor-default">
<OTag type="severity" :value="incidentDetails.severity" />
<OTooltip :content="t('alerts.incidents.severity') + ': ' + incidentDetails.severity" />
<OTooltip
:content="raw(t('alerts.incidents.severity') + ': ' + incidentDetails.severity)"
/>
</span>
<span class="inline-flex cursor-default">
@ -759,7 +761,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ index + 1 }}.
</span>
<div class="min-w-0 flex-1">
<OTooltip v-if="alert.name.length > 30" :content="alert.name" />
<OTooltip
v-if="alert.name.length > 30"
:content="raw(alert.name)"
/>
<span class="block truncate font-medium">
{{
alert.name.length > 30
@ -1363,7 +1368,7 @@ import {
onBeforeUnmount,
onUnmounted,
} from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import { useTheme } from "@/composables/useTheme";
import { useRouter } from "vue-router";
@ -1481,16 +1486,16 @@ export default defineComponent({
// Status and Severity options
const statusOptions = [
{ label: "Open", value: "open" },
{ label: "Acknowledged", value: "acknowledged" },
{ label: "Resolved", value: "resolved" },
{ label: t("alerts.incidents.statusOpen"), value: "open" },
{ label: t("alerts.incidents.statusAcknowledged"), value: "acknowledged" },
{ label: t("alerts.incidents.statusResolved"), value: "resolved" },
];
const severityOptions = [
{ label: "P1 - Critical", value: "P1" },
{ label: "P2 - High", value: "P2" },
{ label: "P3 - Medium", value: "P3" },
{ label: "P4 - Low", value: "P4" },
{ label: t("alerts.incidents.severityP1"), value: "P1" },
{ label: t("alerts.incidents.severityP2"), value: "P2" },
{ label: t("alerts.incidents.severityP3"), value: "P3" },
{ label: t("alerts.incidents.severityP4"), value: "P4" },
];
// Table of Contents
@ -2774,10 +2779,10 @@ export default defineComponent({
});
} else {
const ok = await confirm({
title: "Re-run AI Analysis?",
message: "Severity has changed. Would you like AI to re-analyze this incident?",
confirmLabel: "Re-run AI analysis",
cancelLabel: "No thanks",
title: t("alerts.incidents.rerunAnalysisTitle"),
message: t("alerts.incidents.rerunAnalysisMessage"),
confirmLabel: t("alerts.incidents.rerunAnalysisConfirmLabel"),
cancelLabel: t("alerts.incidents.rerunAnalysisCancelLabel"),
persistent: false,
});
if (ok) {
@ -3355,6 +3360,7 @@ export default defineComponent({
};
return {
raw,
t,
store,
loading,

View File

@ -117,7 +117,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OTag
type="incidentStatus"
:value="row.status"
:label="getStatusLabel(row.status)"
:label="raw(getStatusLabel(row.status))"
size="sm"
data-test="incident-status-badge"
/>
@ -183,7 +183,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
unit="us"
mode="relative"
:timezone="store.state.timezone"
empty-label="—"
:empty-label="raw('')"
/>
</template>
<template #cell-actions="{ row }">
@ -251,7 +251,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, shallowRef, computed, onMounted, watch, nextTick } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import { useRouter, useRoute } from "vue-router";
import { formatToReadable } from "@/utils/date";
@ -813,6 +813,7 @@ export default defineComponent({
]);
return {
raw,
t,
loading,
allIncidents,

View File

@ -137,7 +137,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
side="bottom"
align="start"
:max-width="'24rem'"
:content="getFailureTooltip(event)"
:content="raw(getFailureTooltip(event))"
/>
</span>
<span
@ -286,7 +286,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts" setup>
import { ref, onMounted, watch, nextTick } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useTheme } from "@/composables/useTheme";
import { formatToDateOnly } from "@/utils/date";
import incidentsService from "@/services/incidents";

View File

@ -96,7 +96,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:data-test="'organizationdeduplication-fingerprint-' + group.id + '-checkbox'"
:key="group.id"
:value="group.id"
:label="`${group.display} (${group.id})`"
:label="raw(`${group.display} (${group.id})`)"
/>
</OFormCheckboxGroup>
</div>
@ -147,7 +147,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup lang="ts">
import { ref, watch } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import alertsService from "@/services/alerts";
import OButton from "@/lib/core/Button/OButton.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";

View File

@ -50,7 +50,7 @@ import { reactive } from "vue";
import { onBeforeMount } from "vue";
import { cloneDeep } from "lodash-es";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import searchService from "@/services/search";
import { b64EncodeUnicode, smartDecodeVrlFunction } from "@/utils/zincutils";
import OIcon from "@/lib/core/Icon/OIcon.vue";
@ -1013,7 +1013,7 @@ const refreshData = () => {
];
let yAxis: Array<{
label: string;
label: I18nText;
alias: string;
column: string;
color: string | null;
@ -1080,7 +1080,7 @@ const refreshData = () => {
// Configure y-axis for zo_sql_num (counts)
yAxis = [
{
label: "count",
label: raw("count"),
alias: "zo_sql_num",
column: "zo_sql_num",
color: "#5960b2",

View File

@ -246,7 +246,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
side="top"
align="center"
:max-width="'520px'"
:content="localSqlQueryErrorMsg || sqlQueryErrorMsg"
:content="raw(localSqlQueryErrorMsg || sqlQueryErrorMsg)"
/>
</div>
</div>
@ -318,7 +318,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:query="vrlFunctionContent"
:hide-nl-toggle="false"
:disable-ai="false"
:disable-ai-reason="''"
:disable-ai-reason="raw('')"
:ai-placeholder="t('search.askAIFunctionPlaceholder')"
:ai-tooltip="t('search.enterFunctionPrompt')"
:debounce-time="300"
@ -514,7 +514,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup lang="ts">
import { ref, computed, watch, type PropType, onMounted, inject, type Ref } from "vue";
import { type SqlErrorRange } from "@/utils/query/sqlDiagnostics";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import useTheme from "@/composables/useTheme";
import OButton from "@/lib/core/Button/OButton.vue";

View File

@ -115,7 +115,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:key="group.id"
v-model="localFingerprintFields"
:value="group.id"
:label="group.display"
:label="raw(group.display)"
class="min-w-50"
@update:model-value="emitUpdate"
/>
@ -139,7 +139,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts" setup>
import { ref, computed, watch, onMounted, nextTick } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { v4 as uuidv4 } from "uuid";
import SemanticGroupItem from "./SemanticGroupItem.vue";
import ImportSemanticGroupsDrawer from "./ImportSemanticGroupsDrawer.vue";
@ -248,7 +248,7 @@ const categoryOptions = computed(() => {
return Array.from(groupsMap.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([category, count]) => ({
label: category,
label: raw(category),
value: category,
count: count,
}));

View File

@ -224,7 +224,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, computed, inject, type PropType, type Ref } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import { getUUID } from "@/utils/zincutils";
import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
@ -269,8 +269,8 @@ export default defineComponent({
default: () => [],
},
description: {
type: String,
default: "",
type: String as unknown as PropType<I18nText>,
default: raw(""),
},
rowTemplate: {
type: String,
@ -404,6 +404,7 @@ export default defineComponent({
};
return {
raw,
t,
store,
variableRows,

View File

@ -232,7 +232,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { computed, defineComponent, inject, onMounted, ref, type PropType } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import OFormInput from "@/lib/forms/Input/OFormInput.vue";
@ -346,7 +346,7 @@ export default defineComponent({
(config.isEnterprise === "true" || config.isCloud === "true") &&
store.state.zoConfig?.workflows_enabled === true,
);
const workflowOptions = ref<{ label: string; value: string }[]>([]);
const workflowOptions = ref<{ label: I18nText; value: string }[]>([]);
const fetchWorkflows = async () => {
if (!workflowsEnabled.value) return;
try {
@ -355,7 +355,8 @@ export default defineComponent({
);
const list = Array.isArray(res.data) ? res.data : (res.data?.list ?? []);
workflowOptions.value = list.map((wf: any) => ({
label: wf.name,
// Workflow names come from the server never translated copy.
label: raw(wf.name),
value: wf.id,
}));
} catch {

View File

@ -214,7 +214,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ t("alerts.compareWithPast.addComparisonWindow") }}
<OTooltip
v-if="isComparisonDisabled"
:content="comparisonDisabledTooltip"
:content="raw(comparisonDisabledTooltip)"
side="top"
align="center"
:sideOffset="8"
@ -229,7 +229,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, watch, computed, inject, type PropType } from "vue";
import { useI18nTyped, type I18nKey } from "@/types/i18n";
import { raw, useI18nTyped, type I18nKey } from "@/types/i18n";
import { useStore } from "vuex";
import { getUUID } from "@/utils/zincutils";
import CustomDateTimePicker from "@/components/CustomDateTimePicker.vue";
@ -396,6 +396,7 @@ export default defineComponent({
};
return {
raw,
t,
store,
multiWindowContainerRef,

View File

@ -100,7 +100,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OTooltip
:content="
logFunctionOptions.find((o: any) => o.value === selectedFunction)
?.tooltip || ''
?.tooltip || raw('')
"
:delay="400"
/>
@ -317,7 +317,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OTooltip
:content="
logFunctionOptions.find((o: any) => o.value === selectedFunction)
?.tooltip || ''
?.tooltip || raw('')
"
:delay="400"
/>
@ -584,7 +584,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
<OTooltip
v-if="cronTimezone"
:content="cronTimezone"
:content="raw(cronTimezone)"
:delay="300"
side="bottom"
/>
@ -964,7 +964,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
<OTooltip
v-if="cronTimezone"
:content="cronTimezone"
:content="raw(cronTimezone)"
:delay="300"
side="bottom"
/>
@ -1176,7 +1176,7 @@ import {
type Ref,
} from "vue";
import { type SqlErrorRange } from "@/utils/query/sqlDiagnostics";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import {
b64EncodeUnicode,
@ -2087,11 +2087,11 @@ export default defineComponent({
value: "custom",
},
{
label: "SQL",
label: raw("SQL"),
value: "sql",
},
{
label: "PromQL",
label: raw("PromQL"),
value: "promql",
},
];
@ -2104,7 +2104,7 @@ export default defineComponent({
value: "custom",
},
{
label: "SQL",
label: raw("SQL"),
value: "sql",
},
];
@ -2770,6 +2770,7 @@ export default defineComponent({
});
return {
raw,
t,
store,
highlightedSqlQuery,

View File

@ -51,7 +51,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OFormSelect
:name="`filters[${idx}].field`"
:options="filteredStreamFields"
:placeholder="filter.field ? '' : t('alerts.anomaly.fieldPlaceholder')"
:placeholder="filter.field ? raw('') : t('alerts.anomaly.fieldPlaceholder')"
class="alert-v3-select filter-field-select"
style="width: 200px"
:loading="loadingFields"
@ -111,7 +111,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:show-auto-complete="true"
:disable-ai="!config.stream_name"
:disable-ai-reason="
!config.stream_name ? t('alerts.anomaly.selectStreamFirst') : ''
!config.stream_name ? t('alerts.anomaly.selectStreamFirst') : raw('')
"
editor-height="100%"
data-test="anomaly-custom-sql"
@ -184,7 +184,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="detectionFunction && detectionFunction !== 'count'"
name="detection_function_field"
:options="filteredDetectionFields"
:placeholder="detectionFunctionField ? '' : t('alerts.anomaly.fieldPlaceholder')"
:placeholder="
detectionFunctionField ? raw('') : t('alerts.anomaly.fieldPlaceholder')
"
:loading="loadingFields"
data-test="anomaly-detection-function-field"
class="alert-v3-select"
@ -562,11 +564,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
label-always
markers
:marker-labels="[
{ value: 0, label: '0' },
{ value: 25, label: '25' },
{ value: 50, label: '50' },
{ value: 75, label: '75' },
{ value: 100, label: '100' },
{ value: 0, label: raw('0') },
{ value: 25, label: raw('25') },
{ value: 50, label: raw('50') },
{ value: 75, label: raw('75') },
{ value: 100, label: raw('100') },
]"
class="sensitivity-range-slider mt-3.5 h-36.25! [--color-slider-thumb-border:white] [--color-slider-thumb:var(--color-accent)] [--color-slider-track-fill:var(--color-accent)] [--color-slider-value:var(--color-text-secondary)]"
data-test="anomaly-threshold-range"
@ -583,7 +585,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { computed, defineComponent, ref, watch, type PropType } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import streamService from "@/services/stream";
import {
@ -647,7 +649,7 @@ export default defineComponent({
// "SQL" stays a literal a proper noun, not translatable copy.
const queryTabOptions = computed(() => [
{ label: t("alerts.queryBuilder"), value: "filters" },
{ label: "SQL", value: "custom_sql" },
{ label: raw("SQL"), value: "custom_sql" },
]);
const filterOperators = ANOMALY_FILTER_OPERATORS;
@ -1192,6 +1194,7 @@ export default defineComponent({
});
return {
raw,
t,
store,
form,

View File

@ -240,7 +240,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { computed, defineComponent, inject, ref } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import OButton from "@/lib/core/Button/OButton.vue";
import OFormInput from "@/lib/forms/Input/OFormInput.vue";
import OFormTextarea from "@/lib/forms/Input/OFormTextarea.vue";
@ -281,12 +281,12 @@ export default defineComponent({
const secretType = select((s) => s?.values?.key?.store?.akeyless?.store?.type, "");
const authenticationTypeOptions = [
{ label: "Access Key", value: "access_key" },
{ label: "LDAP", value: "ldap" },
{ label: t("cipherKey.accessKey"), value: "access_key" },
{ label: raw("LDAP"), value: "ldap" },
];
const secretTypeOptions = [
{ label: "Static Secret", value: "static_secret" },
{ label: "DFC", value: "dfc" },
{ label: t("cipherKey.staticSecret"), value: "static_secret" },
{ label: raw("DFC"), value: "dfc" },
];
const getSecretOptionLabel = (value: string) =>
@ -296,6 +296,7 @@ export default defineComponent({
authenticationTypeOptions.find((option) => option.value === value)?.label ?? "";
return {
raw,
t,
authenticationTypeOptions,
secretTypeOptions,

View File

@ -63,7 +63,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OStep
data-test="cipher-key-key-store-detils-step"
:name="1"
:title="step1Title"
:title="raw(step1Title)"
icon="edit"
:done="step > 1"
>
@ -166,7 +166,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onActivated } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import AddOpenobserveType from "@/components/cipherkeys/AddOpenobserveType.vue";
@ -204,8 +204,8 @@ const pendingContinue = ref(false);
const originalData = ref("");
const cipherKeyTypes = [
{ label: "OpenObserve", value: "local" },
{ label: "Akeyless", value: "akeyless" },
{ label: raw("OpenObserve"), value: "local" },
{ label: raw("Akeyless"), value: "akeyless" },
];
const dialog = ref({

View File

@ -47,7 +47,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { computed, defineComponent, inject } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import OFormSelect from "@/lib/forms/Select/OFormSelect.vue";
import { FORM_CONTEXT_KEY } from "@/lib/forms/Form/OForm.types";
@ -66,13 +66,14 @@ export default defineComponent({
: computed(() => "simple");
const providerTypeOptions = [
{ value: "simple", label: "Simple" },
{ value: "tink_keyset", label: "Tink KeySet" },
{ value: "simple", label: t("cipherKeys.mechanismSimple") },
{ value: "tink_keyset", label: raw("Tink KeySet") },
];
const plainAlgorithmOptions = [{ value: "aes-256-siv", label: "AES 256 SIV" }];
const plainAlgorithmOptions = [{ value: "aes-256-siv", label: raw("AES 256 SIV") }];
return {
raw,
t,
mechanismType,
providerTypeOptions,

View File

@ -38,12 +38,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:data-test="`tab-${tab.value}-dirty-dot`"
aria-hidden="true"
/>
<OTooltip v-if="tab.tooltipLabel" :content="tab.tooltipLabel" />
<OTooltip v-if="tab.tooltipLabel" :content="raw(tab.tooltipLabel)" />
</OToggleGroupItem>
</OToggleGroup>
</template>
<script setup lang="ts">
import { raw, type I18nText } from "@/types/i18n";
import { computed } from "vue";
import type { Component } from "vue";
import OToggleGroup from "@/lib/core/ToggleGroup/OToggleGroup.vue";
@ -53,11 +54,11 @@ import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import type { ToggleGroupItemSize } from "@/lib/core/ToggleGroup/OToggleGroupItem.types";
interface Tab {
label: string;
label: I18nText;
value: string;
style?: Record<string, string>;
disabled?: boolean;
title?: string;
title?: I18nText;
tooltipLabel?: string;
hide?: boolean;
icon?: Component | string;
@ -75,12 +76,12 @@ const props = withDefaults(
activeTab: string;
size?: ToggleGroupItemSize;
// Tooltip shown when hovering an unsaved-changes dot (optional).
dirtyTitle?: string;
dirtyTitle?: I18nText;
}>(),
{
show: true,
size: "sm",
dirtyTitle: "",
dirtyTitle: raw(""),
},
);

View File

@ -26,7 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OPageHeader
v-if="!hideHeader"
:title="title"
:back="{ label: '', onClick: handleBack, dataTest: `${testPrefix}-import-back-btn` }"
:back="{ label: raw(''), onClick: handleBack, dataTest: `${testPrefix}-import-back-btn` }"
class="border-border-default shrink-0 border-b"
:class="headerContainerClass"
>
@ -78,7 +78,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<AppTabs
:data-test="`${testPrefix}-import-tabs`"
class="tabs-selection-container"
:tabs="tabs"
:tabs="resolvedTabs"
v-model:active-tab="activeTab"
@update:active-tab="handleTabChange"
/>
@ -131,7 +131,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:label="t('dashboard.dropFileMsg')"
accept=".json"
multiple
helpText=".json files only"
:helpText="t('common.jsonFilesOnlyHint')"
>
<template v-slot:prepend>
<OIcon name="cloud-upload" size="sm" @click.stop.prevent />
@ -207,7 +207,7 @@ import {
onBeforeUnmount,
type PropType,
} from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
import axios from "axios";
import AppTabs from "./AppTabs.vue";
import OPageHeader from "@/lib/core/PageHeader/OPageHeader.vue";
@ -235,26 +235,19 @@ export default defineComponent({
props: {
// Title for the import page
title: {
type: String,
type: String as unknown as PropType<I18nText>,
required: true,
},
// Tabs configuration (shape matches AppTabs' Tab interface)
// Tabs configuration (shape matches AppTabs' Tab interface).
// No `default` here on purpose: the fallback tabs carry translated labels and
// a prop default factory runs outside the component's i18n context, which
// would freeze them at the locale active when the first instance mounted.
// `resolvedTabs` in setup() builds them with `t()` instead see below.
tabs: {
type: Array as PropType<
{ label: string; value: string; icon?: string; disabled?: boolean }[]
{ label: I18nText; value: string; icon?: string; disabled?: boolean }[]
>,
default: () => [
{
label: "File Upload / JSON",
value: "import_json_file",
icon: "upload",
},
{
label: "URL Import",
value: "import_json_url",
icon: "link",
},
],
required: false,
},
// Default active tab
defaultActiveTab: {
@ -354,6 +347,17 @@ export default defineComponent({
}
};
// Tabs the page actually renders: the host's `tabs` prop when given,
// otherwise the built-in File-upload / URL pair, translated here (inside
// setup) so the labels follow the active locale.
const resolvedTabs = computed(
() =>
props.tabs ?? [
{ label: t("common.fileUploadJsonTab"), value: "import_json_file", icon: "upload" },
{ label: t("common.urlImportTab"), value: "import_json_url", icon: "link" },
],
);
// Computed styles
const contentStyle = computed(() => {
return "width: 100%;";
@ -516,12 +520,14 @@ export default defineComponent({
});
return {
raw,
t,
jsonStr,
jsonFiles,
url,
jsonArrayOfObj,
activeTab,
resolvedTabs,
splitterModel,
editorKey,
isImporting,

View File

@ -28,17 +28,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { getImageURL } from "@/utils/zincutils";
import { computed, defineComponent } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { computed, defineComponent, type PropType } from "vue";
import { useI18nTyped, raw, type I18nText } from "@/types/i18n";
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
export default defineComponent({
name: "GroupHeader",
components: { OSeparator },
props: {
// User-facing text: typed I18nText so a bare literal at the call site is a
// compile error. The double cast is required because `StringConstructor`
// returns plain `string`, which does not overlap the branded `I18nText`
// `<script setup>` components can use `defineProps<{ title: I18nText }>()`
// directly and avoid this.
title: {
type: String,
default: "",
type: String as unknown as PropType<I18nText>,
default: raw(""),
},
iconPath: {
type: String,

View File

@ -57,8 +57,16 @@
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, watch, computed, defineAsyncComponent } from "vue";
import { useI18nTyped } from "@/types/i18n";
import {
defineComponent,
ref,
onMounted,
watch,
computed,
defineAsyncComponent,
type PropType,
} from "vue";
import { type I18nText, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import { getImageURL } from "@/utils/zincutils";
import O2AIChat from "../O2AIChat.vue";
@ -79,7 +87,7 @@ export default defineComponent({
required: true,
},
title: {
type: String,
type: String as unknown as PropType<I18nText>,
required: true,
},
type: {

View File

@ -67,12 +67,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script setup lang="ts">
import { raw, type I18nText } from "@/types/i18n";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import type { IconName } from "@/lib/core/Icon/OIcon.icons";
withDefaults(
defineProps<{
label?: string;
label?: I18nText;
/** Override the label's typography classes. Omit for the compact default. */
labelClass?: string;
icon?: IconName;
@ -89,7 +90,7 @@ withDefaults(
dataTest?: string;
}>(),
{
label: "",
label: raw(""),
labelClass: undefined,
icon: undefined,
iconClass: undefined,

View File

@ -303,7 +303,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup lang="ts">
import { computed, ref } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
import { useAiIcon } from "@/composables/useAiIcon";
import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
import EmptyStateActionCard from "@/lib/core/EmptyState/EmptyStateActionCard.vue";
@ -335,9 +335,9 @@ const props = withDefaults(
/** Override the default "broken-panel" illustration (hero size only). */
illustration?: IllustrationName;
/** Override the error code's default title. */
title?: string;
title?: I18nText;
/** Override the error code's default description. */
description?: string;
description?: I18nText;
}>(),
{ size: "hero", aiEnabled: false },
);

View File

@ -19,8 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script lang="ts">
import { raw, type I18nText } from "@/types/i18n";
import { timestampToTimezoneDate } from "@/utils/zincutils";
import { ref, onMounted, onBeforeUnmount, watch, computed } from "vue";
import { ref, onMounted, onBeforeUnmount, watch, computed, type PropType } from "vue";
import { useStore } from "vuex";
export default {
@ -31,9 +32,9 @@ export default {
default: null,
},
fullTimePrefix: {
type: String,
type: String as unknown as PropType<I18nText>,
required: false,
default: "",
default: raw(""),
},
},
setup(props) {
@ -110,6 +111,7 @@ export default {
});
return {
raw,
relativeTime,
formattedExactTime,
};

View File

@ -89,6 +89,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script setup lang="ts">
import type { I18nText } from "@/types/i18n";
import { computed } from "vue";
import { useRouter, type RouteLocationRaw } from "vue-router";
import OIcon from "@/lib/core/Icon/OIcon.vue";
@ -98,8 +99,8 @@ const router = useRouter();
export interface SectionHubItem {
key: string;
label: string;
description?: string;
label: I18nText;
description?: I18nText;
icon?: string;
to: RouteLocationRaw;
visible?: boolean;
@ -107,14 +108,14 @@ export interface SectionHubItem {
}
export interface SectionHubGroup {
label: string;
label: I18nText;
items: SectionHubItem[];
}
const props = defineProps<{
title?: string;
title?: I18nText;
/** Optional one-line description shown under the hub title. */
description?: string;
description?: I18nText;
groups: SectionHubGroup[];
}>();

View File

@ -68,6 +68,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script setup lang="ts">
import type { I18nText } from "@/types/i18n";
import { computed } from "vue";
import { useRouter, type RouteLocationRaw } from "vue-router";
import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
@ -92,7 +93,7 @@ const props = defineProps<{
/** Currently-active section key (highlighted). */
activeKey?: string;
/** Optional small heading shown above the groups (e.g. the module name). */
title?: string;
title?: I18nText;
}>();
// Drop hidden items/empty groups (each item may carry a `visible` flag).

View File

@ -44,7 +44,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, onBeforeUnmount, computed, type PropType } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import { copyToClipboard } from "@/utils/clipboard";
import OButton from "@/lib/core/Button/OButton.vue";
@ -85,8 +85,8 @@ export default defineComponent({
},
// Custom tooltip text
tooltip: {
type: String,
default: "",
type: String as unknown as PropType<I18nText>,
default: raw(""),
},
// Optional keyboard-shortcut hint shown in the tooltip (raw key, e.g. "ctrl+shift+c")
shortcut: {
@ -308,6 +308,7 @@ export default defineComponent({
});
return {
raw,
t,
isLoading,
isWebUrlNotConfigured,

View File

@ -83,13 +83,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script setup lang="ts">
import { raw, type I18nText } from "@/types/i18n";
withDefaults(
defineProps<{
title?: string;
description?: string;
title?: I18nText;
description?: I18nText;
/** Optional eyebrow label rendered above the actions (e.g. "Quick start"). */
actionsLabel?: string;
}>(),
{ title: "", description: "", actionsLabel: "" },
{ title: raw(""), description: raw(""), actionsLabel: "" },
);
</script>

View File

@ -46,12 +46,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script setup lang="ts">
import type { I18nText } from "@/types/i18n";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import type { IconName } from "@/lib/core/Icon/OIcon.icons";
defineProps<{
icon: IconName;
label: string;
sublabel?: string;
label: I18nText;
sublabel?: I18nText;
}>();
</script>

View File

@ -114,6 +114,11 @@ const mockI18n = createI18n({
search: {
searchField: "Search field",
},
// The component passes t("common.valueCopiedToClipboard") to
// copyToClipboard; without the key this mock echoes the key path.
common: {
valueCopiedToClipboard: "Value copied to clipboard",
},
},
},
});

View File

@ -654,7 +654,7 @@ const addSearchTerm = (term: string) => {
};
const copyContentValue = (value: string) => {
copyToClipboard(value, t, { successMessage: "Value copied to clipboard" });
copyToClipboard(value, t, { successMessage: t("common.valueCopiedToClipboard") });
};
</script>

View File

@ -112,7 +112,7 @@
<script lang="ts">
import { defineComponent, ref, watch, computed, type PropType } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import CrossLinkUserGuide from "./CrossLinkUserGuide.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
@ -252,7 +252,7 @@ export default defineComponent({
const added = new Set((formFields.value ?? []).map((f) => f.name));
return (props.availableFields || [])
.filter((name) => !added.has(name))
.map((name) => ({ label: name, value: name }));
.map((name) => ({ label: raw(name), value: name }));
});
function onFieldSelect(value: string) {

View File

@ -91,7 +91,10 @@ describe("CrossLinkManager Component", () => {
describe("Props Default Values", () => {
it("should default title to 'Cross-Links'", () => {
wrapper = createWrapper({ title: undefined });
expect(wrapper.vm.$options.props.title.default).toBe("Cross-Links");
// The default is no longer a literal on the prop — a literal there would ship
// untranslated and t() at module scope would freeze the locale. It is resolved
// per-render in setup(), so assert what the user actually sees.
expect(wrapper.text()).toContain("Cross-Links");
});
it("should default subtitle to empty string", () => {

View File

@ -3,7 +3,7 @@
<!-- Header -->
<div class="mb-3 flex items-center justify-between">
<div>
<div class="text-base font-bold">{{ title }}</div>
<div class="text-base font-bold">{{ resolvedTitle }}</div>
<div v-if="subtitle" class="text-text-muted text-xs">
{{ subtitle }}
</div>
@ -100,7 +100,7 @@
<script lang="ts">
import { defineComponent, ref, computed, type PropType } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, type I18nText, useI18nTyped } from "@/types/i18n";
import CrossLinkDialog from "./CrossLinkDialog.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OTag from "@/lib/core/Badge/OTag.vue";
@ -121,12 +121,13 @@ export default defineComponent({
default: () => [],
},
title: {
type: String,
default: "Cross-Links",
type: String as unknown as PropType<I18nText>,
// Resolved in setup so the fallback is translated, not frozen at load.
default: undefined,
},
subtitle: {
type: String,
default: "",
type: String as unknown as PropType<I18nText>,
default: raw(""),
},
readonly: {
type: Boolean,
@ -145,6 +146,7 @@ export default defineComponent({
setup(props, { emit }) {
const store = useStore();
const { t } = useI18nTyped();
const resolvedTitle = computed(() => props.title ?? t("common.crossLinks"));
const showAddDialog = ref(false);
const editingLink = ref<CrossLink | null>(null);
const editingOriginalName = ref("");
@ -192,6 +194,9 @@ export default defineComponent({
}
return {
resolvedTitle,
raw,
t,
store,
links,

View File

@ -789,7 +789,7 @@ describe("AddDashboardFromGitHub Component", () => {
];
await wrapper.vm.$nextTick();
const drawer = wrapper.find('[data-test-stub="o-drawer"]');
expect(drawer.attributes("data-primary-label")).toBe("Add 1 dashboard(s)");
expect(drawer.attributes("data-primary-label")).toBe("Add 1 dashboard");
});
it("should disable the ODrawer primary button when no dashboards selected", async () => {

View File

@ -43,7 +43,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-else-if="error"
preset="load-error"
size="hero"
:description="error"
:description="raw(error)"
:action-label="t('dashboard.addDashboardFromGitHub.retry')"
@action="loadDashboards"
/>
@ -204,7 +204,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, computed, watch } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import dashboardsService from "@/services/dashboards";
import AddFolder from "@/components/dashboards/AddFolder.vue";
import OButton from "@/lib/core/Button/OButton.vue";
@ -223,7 +223,7 @@ import { toast } from "@/lib/feedback/Toast/useToast";
interface GitHubDashboard {
name: string;
displayName: string;
description?: string;
description?: I18nText;
folderPath: string;
jsonFiles: string[];
}
@ -266,7 +266,7 @@ export default defineComponent({
const selectedDashboards = ref<GitHubDashboard[]>([]);
const showFolderSelection = ref(false);
const selectedFolderObj = ref<string | null>(null);
const folderOptions = ref<{ label: string; value: string }[]>([]);
const folderOptions = ref<{ label: I18nText; value: string }[]>([]);
const importing = ref(false);
const preparing = ref(false);
const showAddFolderDialog = ref(false);
@ -434,7 +434,7 @@ export default defineComponent({
];
folderOptions.value = sorted.map((f: any) => ({
label: f.name,
label: raw(f.name),
value: f.folderId,
}));
@ -656,6 +656,7 @@ export default defineComponent({
});
return {
raw,
t,
show,
loading,

View File

@ -13,6 +13,8 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import type { I18nText } from "@/types/i18n";
import type { Ref } from "vue";
// ============================================================================
@ -97,8 +99,8 @@ export interface PanelEditorChartData {
version?: number;
id?: string;
type?: string;
title?: string;
description?: string;
title?: I18nText;
description?: I18nText;
config?: Record<string, any>;
htmlContent?: string;
markdownContent?: string;
@ -152,8 +154,8 @@ export interface PanelEditorChartData {
*/
export interface PanelEditorDashboardData {
dashboardId?: string;
title?: string;
description?: string;
title?: I18nText;
description?: I18nText;
tabs?: any[];
variables?: {
list?: any[];

View File

@ -70,7 +70,7 @@
</div>
<div class="group relative mt-1">
<div
class="rounded-default bg-theme-body-bg-primary border-card-glass-border inspector-query-editor max-h-40 overflow-y-auto border p-2 font-mono text-sm break-all whitespace-pre-wrap [scrollbar-color:color-mix(in_srgb,var(--color-grey-500)_20%,transparent)_transparent] [scrollbar-width:thin]"
class="rounded-default bg-theme-body-bg-primary border-card-glass-border inspector-query-editor max-h-40 [scrollbar-width:thin] [scrollbar-color:color-mix(in_srgb,var(--color-grey-500)_20%,transparent)_transparent] overflow-y-auto border p-2 font-mono text-sm break-all whitespace-pre-wrap"
:data-test="`query-inspector-original-query-${index}`"
v-html="
highlightSearch(
@ -99,7 +99,7 @@
</div>
<div class="group relative mt-1">
<div
class="rounded-default bg-theme-body-bg-primary border-card-glass-border inspector-query-editor max-h-40 overflow-y-auto border p-2 font-mono text-sm break-all whitespace-pre-wrap [scrollbar-color:color-mix(in_srgb,var(--color-grey-500)_20%,transparent)_transparent] [scrollbar-width:thin]"
class="rounded-default bg-theme-body-bg-primary border-card-glass-border inspector-query-editor max-h-40 [scrollbar-width:thin] [scrollbar-color:color-mix(in_srgb,var(--color-grey-500)_20%,transparent)_transparent] overflow-y-auto border p-2 font-mono text-sm break-all whitespace-pre-wrap"
:data-test="`query-inspector-executed-query-${index}`"
v-html="
highlightSearch(

View File

@ -80,7 +80,7 @@
import { ref, computed, watch } from "vue";
import type { PropType } from "vue";
import { useStore } from "vuex";
import { useI18nTyped, raw } from "@/types/i18n";
import { useI18nTyped, raw, type I18nText } from "@/types/i18n";
import { useLoading } from "@/composables/useLoading";
import { annotationService } from "@/services/dashboard_annotations";
import useNotifications from "@/composables/useNotifications";
@ -95,7 +95,7 @@ import type { AddAnnotationForm } from "./AddAnnotation.schema";
interface AnnotationData {
annotation_id: string | null;
title: string;
title: I18nText;
text: string;
start_time: number | null;
end_time: number | null;
@ -105,7 +105,7 @@ interface AnnotationData {
interface AnnotationPanel {
id: string;
title: string;
title: I18nText;
tabName?: string;
}
@ -136,7 +136,7 @@ const showDeleteConfirm = ref(false);
const annotationData = ref<AnnotationData>(
props.annotation || {
annotation_id: null,
title: "",
title: raw(""),
text: "",
start_time: null,
end_time: null,
@ -158,9 +158,9 @@ const groupedPanels = ref<Record<string, AnnotationPanel[]>>({});
const groupedPanelsOptions = computed(() =>
Object.entries(groupedPanels.value).flatMap(([tab, panels]) => [
{ label: tab, isTab: true, disable: true },
{ label: raw(tab), isTab: true, disable: true },
...panels.map((panel) => ({
label: panel.title,
label: raw(panel.title),
value: panel.id,
isTab: false,
})),
@ -286,7 +286,7 @@ const confirmDelete = async () => {
// reads consistent values. Plain async OForm awaits it, and the ODialog
// built-in primary button (form-id) auto-shows the Save spinner (no useLoading).
const saveAnnotation = async (value: AddAnnotationForm) => {
if (value?.title != null) annotationData.value.title = value.title;
if (value?.title != null) annotationData.value.title = raw(value.title);
annotationData.value.text = value?.text ?? "";
annotationData.value.panels = value?.panels ?? [];
await handleSave();

View File

@ -30,12 +30,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script setup lang="ts">
import type { I18nText } from "@/types/i18n";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
/** Full chip label, e.g. "histogram(_timestamp, acos(actual_value))" */
label: string;
label: I18nText;
/** Token utility for every function/aggregation name (nested included) */
fnClass?: string;
/** Token utility for the emphasized field/column name */

View File

@ -144,7 +144,7 @@ import useDashboardPanelData from "@/composables/dashboard/useDashboardPanel";
import { getColorPalette } from "@/utils/dashboard/colorPalette";
import { computed, inject, onBeforeMount, defineComponent } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
import OToggleGroup from "@/lib/core/ToggleGroup/OToggleGroup.vue";
import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
import OSelect from "@/lib/forms/Select/OSelect.vue";
@ -153,9 +153,9 @@ import OSelectGroup from "@/lib/forms/Select/OSelectGroup.vue";
import type { SelectModelValue } from "@/lib/forms/Select/OSelect.types";
interface ColorOption {
label: string;
label: I18nText;
value?: string;
subLabel?: string;
subLabel?: I18nText;
colorPalette?: string[];
header?: boolean;
}

View File

@ -151,7 +151,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="dashboard-config-panel-time-picker"
class="w-fit max-w-full min-w-0 overflow-hidden"
/>
<OTooltip :content="formattedPickerValue" max-width="320px" />
<OTooltip :content="raw(formattedPickerValue)" max-width="320px" />
</div>
<OIcon
class="mr-1 ml-2 flex-shrink-0 shrink-0 cursor-pointer"
@ -440,7 +440,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-for="(tab, index) in dashboardPanelData.data.queries"
:key="index"
:name="index"
:label="`${t('dashboard.queryLabel')} ${Number(index) + 1}`"
:label="raw(`${t('dashboard.queryLabel')} ${Number(index) + 1}`)"
:data-test="`dashboard-config-query-tab-${index}`"
>
</OTab>
@ -603,7 +603,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</div>
<OInput
placeholder="{field_name}"
:placeholder="raw('{field_name}')"
v-model="
dashboardPanelDataModel.data.queries[dashboardPanelData.layout.currentQueryIndex]
.config.query_label
@ -719,7 +719,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="shouldShowNoValueReplacement(dashboardPanelData, promqlMode)"
v-show="isConfigOptionVisible('data', 'no-value-replacement')"
v-model="dashboardPanelDataModel.data.config.no_value_replacement"
placeholder="-"
:placeholder="raw('-')"
:label="t('dashboard.noValueReplacement')"
data-test="dashboard-config-no-value-replacement"
>
@ -1729,7 +1729,7 @@ import { type SwitchValue } from "@/lib/forms/Switch/OSwitch.types";
import useDashboardPanelData from "@/composables/dashboard/useDashboardPanel";
import { getUnitOptions } from "@/composables/dashboard/useColumnFormatting";
import { computed, defineComponent, inject, nextTick, onBeforeMount, onMounted, ref } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import Drilldown from "./Drilldown.vue";
import ValueMapping from "./ValueMapping.vue";
import ColorBySeries from "./ColorBySeries.vue";
@ -2150,8 +2150,10 @@ export default defineComponent({
value: "center",
},
];
// Single source of truth shared with the column-formatting dialog.
const unitOptions = getUnitOptions(t);
// Single source of truth shared with the column-formatting dialog. Its
// labels are already translated (it is handed `t`), but the helper's
// signature widens them back to `string`, so re-brand them for OSelect.
const unitOptions = getUnitOptions(t).map((o) => ({ ...o, label: raw(o.label) }));
const labelPositionOptions = [
{
@ -2306,7 +2308,7 @@ export default defineComponent({
return streamFields.schema.map((it: any) => {
return {
label: it.name,
label: raw(it.name),
value: it.name,
};
});
@ -2557,6 +2559,7 @@ export default defineComponent({
const decimalsTouched = ref(false);
return {
raw,
legendsPositionModel,
legendsTypeModel,
chartAlignModel,

View File

@ -211,7 +211,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"
:hide-nl-toggle="false"
:disable-ai="false"
:disable-ai-reason="''"
:disable-ai-reason="raw('')"
:ai-placeholder="t('function.askAIFunctionPlaceholder')"
:ai-tooltip="t('function.enterFunctionPrompt')"
editor-height="100%"
@ -276,7 +276,7 @@ import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
import OTab from "@/lib/navigation/Tabs/OTab.vue";
// @ts-nocheck
import { defineComponent, ref, watch, computed, onMounted, nextTick, onUnmounted } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useRouter } from "vue-router";
import useDashboardPanelData from "../../../composables/dashboard/useDashboardPanel";
import QueryTypeSelector from "../addPanel/QueryTypeSelector.vue";
@ -892,6 +892,7 @@ export default defineComponent({
};
return {
raw,
t,
router,
onDropDownClick,

View File

@ -16,7 +16,7 @@
</div>
<Teleport to="body">
<div
class="user-guide border-border-default rounded-default bg-surface-base pointer-events-auto fixed z-9999 max-h-75 w-125 overflow-y-auto border p-2.5 [scrollbar-color:color-mix(in_srgb,var(--color-grey-950)_25%,transparent)_color-mix(in_srgb,var(--color-grey-950)_5%,transparent)] [scrollbar-width:thin] [&_div]:m-0 [&_li]:m-0 [&_p]:m-0 [&_ul]:m-0"
class="user-guide border-border-default rounded-default bg-surface-base pointer-events-auto fixed z-9999 max-h-75 w-125 [scrollbar-width:thin] [scrollbar-color:color-mix(in_srgb,var(--color-grey-950)_25%,transparent)_color-mix(in_srgb,var(--color-grey-950)_5%,transparent)] overflow-y-auto border p-2.5 [&_div]:m-0 [&_li]:m-0 [&_p]:m-0 [&_ul]:m-0"
v-show="showUserGuide"
@mouseleave="showUserGuide = false"
ref="userGuideDivRef"

View File

@ -28,14 +28,14 @@
<script lang="ts">
import { defineComponent, ref, computed, inject, onBeforeMount } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
import OverrideConfigPopup from "../OverrideConfigPopup.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import useDashboardPanelData from "../../../composables/dashboard/useDashboardPanel";
interface Column {
alias: string;
label: string;
label: I18nText;
format?: (val: unknown) => string;
}

View File

@ -396,7 +396,7 @@
<script setup lang="ts">
import { computed, ref, watch, onMounted, inject } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import { useTheme } from "@/composables/useTheme";
import useDashboardPanelData from "@/composables/dashboard/useDashboardPanel";
@ -515,7 +515,7 @@ function onStreamChange(val: SelectModelValue) {
// Stream type options
const streamTypeOptions = computed(() =>
["logs", "metrics", "traces"].map((t: string) => ({ label: t, value: t })),
["logs", "metrics", "traces"].map((t: string) => ({ label: raw(t), value: t })),
);
// Stream list

View File

@ -71,7 +71,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script lang="ts">
import { defineComponent, ref, watch, provide } from "vue";
import { type I18nText } from "@/types/i18n";
import { defineComponent, ref, watch, provide, type PropType } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
@ -80,7 +81,7 @@ export default defineComponent({
components: { OSeparator, OButton, OIcon },
props: {
title: {
type: String,
type: String as unknown as PropType<I18nText>,
required: true,
},
modelValue: {

View File

@ -22,6 +22,9 @@ const mockI18n = createI18n({
locale: "en",
messages: {
en: {
common: {
all: "All",
},
dashboard: {
rowsPerPage: "Rows per page",
},

View File

@ -84,7 +84,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, computed } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import OButton from "@/lib/core/Button/OButton.vue";
import OSelect from "@/lib/forms/Select/OSelect.vue";
import type { SelectModelValue } from "@/lib/forms/Select/OSelect.types";
@ -140,12 +140,13 @@ export default defineComponent({
const formattedPaginationOptions = computed(() =>
props.paginationOptions.map((opt) => ({
label: opt === 0 ? "All" : String(opt),
label: opt === 0 ? t("common.all") : raw(String(opt)),
value: opt,
})),
);
return {
raw,
countDisplay,
formattedPaginationOptions,
t,

View File

@ -46,8 +46,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
// @ts-nocheck
import { defineComponent, ref, computed } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { defineComponent, ref, computed, type PropType } from "vue";
import { type I18nText, useI18nTyped } from "@/types/i18n";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OCheckbox from "@/lib/forms/Checkbox/OCheckbox.vue";
import OBanner from "@/lib/feedback/Banner/OBanner.vue";
@ -58,11 +58,11 @@ export default defineComponent({
emits: ["update:ok", "update:cancel", "update:modelValue"],
props: {
title: {
type: String,
type: String as unknown as PropType<I18nText>,
required: true,
},
message: {
type: String,
type: String as unknown as PropType<I18nText>,
required: true,
},
warningMessage: {

View File

@ -131,7 +131,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<img
:src="chart.asset"
:alt="chart.label"
:alt="t(chart.labelKey)"
class="h-full w-full object-cover"
loading="lazy"
/>
@ -139,7 +139,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OCardSection>
<OCardSection class="px-2 pt-0 pb-2">
<div class="text-center text-xs font-medium">
{{ chart.label }}
{{ t(chart.labelKey) }}
</div>
</OCardSection>
</OCard>
@ -218,8 +218,10 @@ export default defineComponent({
const filtered: ChartCategory[] = [];
chartCategories.value.forEach((category) => {
// Search the RESOLVED name, not the key otherwise typing "line" would
// match the dotted path rather than what the user can actually see.
const filteredCharts = category.type.filter((chart) =>
chart.label.toLowerCase().includes(query),
t(chart.labelKey).toLowerCase().includes(query),
);
if (filteredCharts.length > 0) {

View File

@ -1,7 +1,15 @@
import type { I18nKey } from "@/types/i18n";
import { getImageURL } from "@/utils/zincutils";
export interface ChartType {
label: string;
/**
* i18n KEY for the example's display name, not the English text this module
* is a plain, module-scope constant with no `t()` in reach, so resolving here
* would freeze the copy at whatever locale was active on import. The consumer
* (CustomChartTypeSelector.vue) calls `t(labelKey)` at render time instead.
*/
labelKey: I18nKey;
value: string;
asset: string;
}
@ -17,22 +25,22 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Line",
type: [
{
label: "Basic Line Chart",
labelKey: "dashboard.customChartTypeSelector.examples.basicLineChart",
value: "line-simple",
asset: getImageURL("dashboard/CustomChartAssets/line-simple.webp"),
},
{
label: "Confidence Band",
labelKey: "dashboard.customChartTypeSelector.examples.confidenceBand",
value: "confidence-band",
asset: getImageURL("dashboard/CustomChartAssets/confidence-band.webp"),
},
{
label: "Multiple X Axes",
labelKey: "dashboard.customChartTypeSelector.examples.multipleXAxes",
value: "multiple-x-axis",
asset: getImageURL("dashboard/CustomChartAssets/multiple-x-axis.webp"),
},
{
label: "Intraday Line Breaks 1",
labelKey: "dashboard.customChartTypeSelector.examples.intradayLineBreaks1",
value: "intraday-breaks-1",
asset: getImageURL("dashboard/CustomChartAssets/intraday-breaks-1.webp"),
},
@ -42,17 +50,17 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Bar",
type: [
{
label: "Bar Race",
labelKey: "dashboard.customChartTypeSelector.examples.barRace",
value: "bar-race",
asset: getImageURL("dashboard/CustomChartAssets/bar-race.webp"),
},
{
label: "Stacked Bar Normalization",
labelKey: "dashboard.customChartTypeSelector.examples.stackedBarNormalization",
value: "bar-stack-normalization",
asset: getImageURL("dashboard/CustomChartAssets/bar-stack-normalization.webp"),
},
{
label: "Stacked Radial Bar (Polar)",
labelKey: "dashboard.customChartTypeSelector.examples.stackedRadialBarPolar",
value: "bar-polar-stack-radial",
asset: getImageURL("dashboard/CustomChartAssets/bar-polar-stack-radial.webp"),
},
@ -62,12 +70,12 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Pie",
type: [
{
label: "Pie with Border",
labelKey: "dashboard.customChartTypeSelector.examples.pieWithBorder",
value: "pie-border-radius",
asset: getImageURL("dashboard/CustomChartAssets/pie-border-radius.webp"),
},
{
label: "Partition Data to Pies",
labelKey: "dashboard.customChartTypeSelector.examples.partitionDataToPies",
value: "data-transform-multiple-pie",
asset: getImageURL("dashboard/CustomChartAssets/data-transform-multiple-pie.webp"),
},
@ -77,12 +85,12 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Scatter",
type: [
{
label: "Scatter Matrix",
labelKey: "dashboard.customChartTypeSelector.examples.scatterMatrix",
value: "scatter-matrix",
asset: getImageURL("dashboard/CustomChartAssets/scatter-matrix.webp"),
},
{
label: "Scatter Polynomial Regression",
labelKey: "dashboard.customChartTypeSelector.examples.scatterPolynomialRegression",
value: "scatter-polynomial-regression",
asset: getImageURL("dashboard/CustomChartAssets/scatter-polynomial-regression.webp"),
},
@ -92,12 +100,12 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Radar",
type: [
{
label: "Customized Radar Chart",
labelKey: "dashboard.customChartTypeSelector.examples.customizedRadarChart",
value: "radar-custom",
asset: getImageURL("dashboard/CustomChartAssets/radar-custom.webp"),
},
{
label: "Multiple Radar",
labelKey: "dashboard.customChartTypeSelector.examples.multipleRadar",
value: "radar-multiple-2",
asset: getImageURL("dashboard/CustomChartAssets/radar-multiple-2.webp"),
},
@ -107,7 +115,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Boxplot",
type: [
{
label: "Data Transform Simple Aggregate",
labelKey: "dashboard.customChartTypeSelector.examples.dataTransformSimpleAggregate",
value: "data-transform-aggregate",
asset: getImageURL("dashboard/CustomChartAssets/data-transform-aggregate.webp"),
},
@ -117,7 +125,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Graph",
type: [
{
label: "Graph on Cartesian",
labelKey: "dashboard.customChartTypeSelector.examples.graphOnCartesian",
value: "graph-on-cartesian",
asset: getImageURL("dashboard/CustomChartAssets/graph-on-cartesian.webp"),
},
@ -127,7 +135,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Treemap",
type: [
{
label: "Treemap chart",
labelKey: "dashboard.customChartTypeSelector.examples.treemapChart",
value: "treemap-chart",
asset: getImageURL("dashboard/CustomChartAssets/treemap-chart.webp"),
},
@ -137,7 +145,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Funnel",
type: [
{
label: "Customized Funnel",
labelKey: "dashboard.customChartTypeSelector.examples.customizedFunnel",
value: "funnel-customize",
asset: getImageURL("dashboard/CustomChartAssets/funnel-customize.webp"),
},
@ -147,7 +155,7 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "Dataset",
type: [
{
label: "Series Layout",
labelKey: "dashboard.customChartTypeSelector.examples.seriesLayout",
value: "dataset-series-layout",
asset: getImageURL("dashboard/CustomChartAssets/dataset-series-layout.webp"),
},
@ -157,22 +165,22 @@ export const chartTypesData: { data: ChartCategory[] } = {
chartLabel: "3D",
type: [
{
label: "3D Bar with Dataset",
labelKey: "dashboard.customChartTypeSelector.examples.threeDBarWithDataset",
value: "bar3d-dataset",
asset: getImageURL("dashboard/CustomChartAssets/bar3d-dataset.webp"),
},
{
label: "Bar3D Punch Card",
labelKey: "dashboard.customChartTypeSelector.examples.bar3dPunchCard",
value: "bar3d-punchcard",
asset: getImageURL("dashboard/CustomChartAssets/bar3d-punchcard.webp"),
},
{
label: "3D Scatter with Scatter Matrix",
labelKey: "dashboard.customChartTypeSelector.examples.threeDScatterWithScatterMatrix",
value: "scatter3d-scatter-matrix",
asset: getImageURL("dashboard/CustomChartAssets/scatter3d-scatter-matrix.webp"),
},
{
label: "3D Scatter Dataset",
labelKey: "dashboard.customChartTypeSelector.examples.threeDScatterDataset",
value: "scatter3d-dataset",
asset: getImageURL("dashboard/CustomChartAssets/scatter3D-dataset.webp"),
},

View File

@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<DashboardHeader :title="title" backButton @back="close"> </DashboardHeader>
<div
class="[&::-webkit-scrollbar-thumb]:rounded-default [&::-webkit-scrollbar-thumb]:bg-border-default min-h-0 flex-1 overflow-y-auto px-0.75 pb-4 [scrollbar-color:var(--color-border-default)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar]:bg-transparent"
class="[&::-webkit-scrollbar-thumb]:rounded-default [&::-webkit-scrollbar-thumb]:bg-border-default min-h-0 flex-1 [scrollbar-width:thin] [scrollbar-color:var(--color-border-default)_transparent] overflow-y-auto px-0.75 pb-4 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar]:bg-transparent"
>
<OForm greedy id="add-setting-variable-form" :form="form" class="px-0.5">
<div class="mt-3">
@ -204,7 +204,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
valueKey="name"
searchable
:placeholder="
filter.name ? '' : t('dashboard.addSettingVariable.selectFieldPlaceholder')
filter.name
? raw('')
: t('dashboard.addSettingVariable.selectFieldPlaceholder')
"
:title="filter.name || undefined"
@update:model-value="filterUpdated(index, $event)"
@ -529,7 +531,7 @@ import {
computed,
nextTick,
} from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useSelectAutoComplete } from "../../../composables/useSelectAutocomplete";
import { useStore } from "vuex";
import { addVariable, getDashboard, updateVariable } from "../../../utils/commons";
@ -607,7 +609,7 @@ export default defineComponent({
filter: [],
},
value: "",
options: [{ label: "", value: "", selected: true }],
options: [{ label: raw(""), value: "", selected: true }],
multiSelect: false,
hideOnDashboard: false,
selectAllValueForMultiSelect: "first",
@ -689,7 +691,7 @@ export default defineComponent({
// Format tabs for selection from dashboard data
const tabsOptions = computed(() =>
dashboardData.value.tabs.map((tab: any) => ({
label: tab.name,
label: raw(tab.name),
value: tab.tabId,
})),
);
@ -730,7 +732,7 @@ export default defineComponent({
// Add existing panels from this tab
panelOptions.push(
...(tab.panels || []).map((panel: any) => ({
label: panel.title,
label: raw(panel.title),
value: panel.id,
})),
);
@ -788,7 +790,7 @@ export default defineComponent({
]);
const streamTypeOptions = computed(() =>
data.streamType.map((t: string) => ({ label: t, value: t })),
data.streamType.map((t: string) => ({ label: raw(t), value: t })),
);
const handleCustomSelectAll = () => {
@ -1051,7 +1053,7 @@ export default defineComponent({
const addField = () => {
// add new field for options
formPush("options", {
label: "",
label: raw(""),
value: "",
selected: false,
});
@ -1442,7 +1444,7 @@ export default defineComponent({
});
return filteredVars.map((it: any) => ({
label: it.name,
label: raw(it.name),
value: "$" + it.name,
}));
});
@ -1539,6 +1541,7 @@ export default defineComponent({
isSavingVariable,
store,
t,
raw,
data,
streamsFilterFn,
fieldsFilterFn,

View File

@ -55,7 +55,7 @@
import { defineComponent, ref, toRef, watch, type Ref, toRefs } from "vue";
import { useSelectAutoComplete } from "../../../composables/useSelectAutocomplete";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import DynamicFilterIcon from "../../icons/DynamicFilterIcon.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
@ -79,8 +79,8 @@ export default defineComponent({
const store = useStore();
const { t } = useI18nTyped();
const operatorOptions = [
{ label: "=", value: "=" },
{ label: "!=", value: "!=" },
{ label: raw("="), value: "=" },
{ label: raw("!="), value: "!=" },
];
const options = toRef(props.variableItem, "options");
const { modelValue: adhocVariables } = toRefs(props) as {
@ -121,6 +121,7 @@ export default defineComponent({
};
return {
raw,
fieldsFilterFn,
fieldsFilteredOptions,
addFields,

View File

@ -42,7 +42,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { raw, type I18nText } from "@/types/i18n";
import { defineComponent, type PropType } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OSeparator from "@/lib/core/Separator/OSeparator.vue";
@ -51,8 +52,8 @@ export default defineComponent({
components: { OSeparator, OButton },
props: {
title: {
type: String,
default: "",
type: String as unknown as PropType<I18nText>,
default: raw(""),
},
backButton: {
type: Boolean,
@ -66,6 +67,7 @@ export default defineComponent({
};
return {
raw,
onBackClicked,
};
},

View File

@ -37,6 +37,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
testPrefix data-test prefix, so each editor keeps its existing selectors
-->
<script lang="ts">
import type { I18nText } from "@/types/i18n";
// Palette styling lives in its own token-driven stylesheet (see the note at the
// top of node-palette.css for why it cannot be scoped). Imported here rather
// than through an SFC style block, so this component carries none at all.
@ -51,8 +52,8 @@ interface NodePaletteItem {
subtype?: string;
io_type?: string;
isSectionHeader?: boolean;
label?: string;
tooltip?: string;
label?: I18nText;
tooltip?: I18nText;
icon?: string;
[key: string]: unknown;
}

View File

@ -57,7 +57,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<OSearchInput
ref="searchRef"
v-model="search"
:placeholder="placeholderText"
:placeholder="raw(placeholderText)"
clearable
class="w-full"
:data-test="testPrefix + '-search'"
@ -103,14 +103,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped, type I18nText } from "@/types/i18n";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
interface StepItem {
key: string;
title: string;
description?: string;
title: I18nText;
description?: I18nText;
icon?: string;
iconTint?: string;
[k: string]: any;
@ -119,8 +119,8 @@ interface StepItem {
const props = withDefaults(
defineProps<{
items: StepItem[];
searchPlaceholder?: string;
noMatchText?: string;
searchPlaceholder?: I18nText;
noMatchText?: I18nText;
testPrefix?: string;
/** Viewport point to open at (usually the click). Null = screen-centred. */
anchor?: { x: number; y: number } | null;
@ -130,8 +130,8 @@ const props = withDefaults(
// Empty, not English: t() cannot run at module scope (no setup context), so
// the locale fallback lives in the computeds below. A caller may still pass
// its own already-translated string.
searchPlaceholder: "",
noMatchText: "",
searchPlaceholder: raw(""),
noMatchText: raw(""),
testPrefix: "flow-step",
},
);

View File

@ -369,16 +369,16 @@ export default defineComponent({
if (hasFailedJob.value) {
// When there's a failed job, only allow: reload, replace failed, or replace all
return [
{ label: "Reload existing URLs", value: "reload" },
{ label: "Replace failed URL only", value: "replace_failed" },
{ label: "Replace all URLs", value: "replace" },
{ label: t("function.updateModeReload"), value: "reload" },
{ label: t("function.updateModeReplaceFailed"), value: "replace_failed" },
{ label: t("function.updateModeReplaceAll"), value: "replace" },
];
} else {
// Normal mode: reload, append, or replace
return [
{ label: "Reload existing URLs", value: "reload" },
{ label: "Add new URL", value: "append" },
{ label: "Replace all URLs", value: "replace" },
{ label: t("function.updateModeReload"), value: "reload" },
{ label: t("function.updateModeAppend"), value: "append" },
{ label: t("function.updateModeReplaceAll"), value: "replace" },
];
}
});

View File

@ -58,7 +58,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
name="function"
v-model:is-expanded="expandState.functions"
:label="
(transType === '1' ? t('function.jsfunction') : t('function.vrlfunction')) + '*'
raw(
(transType === '1' ? t('function.jsfunction') : t('function.vrlfunction')) +
'*',
)
"
min-header-height="2.125rem"
/>
@ -73,7 +76,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:query="formData.function"
:hide-nl-toggle="!store.state.zoConfig.ai_enabled"
:disable-ai="!store.state.zoConfig.ai_enabled"
:disable-ai-reason="''"
:disable-ai-reason="raw('')"
:ai-placeholder="t('function.askAIFunctionPlaceholder')"
:ai-tooltip="t('function.enterFunctionPrompt')"
editor-height="100%"
@ -182,7 +185,7 @@ import {
} from "vue";
import jsTransformService from "../../services/jstransform";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useStore } from "vuex";
import config from "@/aws-exports";
import segment from "../../services/segment_analytics";
@ -597,6 +600,7 @@ export default defineComponent({
};
return {
raw,
t,
emit,
disableColor,

View File

@ -31,7 +31,8 @@
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { type I18nText } from "@/types/i18n";
import { computed, type PropType } from "vue";
import { useStore } from "vuex";
import OIcon from "@/lib/core/Icon/OIcon.vue";
@ -41,7 +42,7 @@ const props = defineProps({
required: true,
},
label: {
type: String,
type: String as unknown as PropType<I18nText>,
required: true,
},
isExpandable: {

View File

@ -14,7 +14,12 @@
class="text-status-error-text mx-1 cursor-pointer"
size="sm"
>
<OTooltip side="right" align="center" :side-offset="10" :content="sqlQueryErrorMsg" />
<OTooltip
side="right"
align="center"
:side-offset="10"
:content="raw(sqlQueryErrorMsg)"
/>
</OIcon>
</template>
<template #right>
@ -142,7 +147,12 @@
class="text-status-error-text mx-1 cursor-pointer"
size="sm"
>
<OTooltip side="right" align="center" :side-offset="10" :content="eventsErrorMsg" />
<OTooltip
side="right"
align="center"
:side-offset="10"
:content="raw(eventsErrorMsg)"
/>
</OIcon>
</template>
<template #right>
@ -206,7 +216,7 @@
side="right"
align="center"
:side-offset="10"
:content="outputEventsErrorMsg"
:content="raw(outputEventsErrorMsg)"
/>
</OIcon>
</template>
@ -361,9 +371,9 @@ const { getStreams, getStream } = useStreams(t);
const { buildQueryPayload } = useQuery();
const streamTypes = [
{ label: "Logs", value: "logs", icon: "description" },
{ label: "Metrics", value: "metrics", icon: "bar-chart" },
{ label: "Traces", value: "traces", icon: "activity" },
{ label: t("common.logs"), value: "logs", icon: "description" },
{ label: t("common.metrics"), value: "metrics", icon: "bar-chart" },
{ label: t("common.traces"), value: "traces", icon: "activity" },
];
const isFetchingStreams = ref(false);

View File

@ -211,7 +211,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { ref, computed, defineComponent, onBeforeMount } from "vue";
import { useStore } from "vuex";
import { useI18nTyped } from "@/types/i18n";
import { useI18nTyped, type I18nText } from "@/types/i18n";
import OButton from "@/lib/core/Button/OButton.vue";
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
@ -240,7 +240,7 @@ import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
interface Token {
name: string;
token: string;
description: string;
description: I18nText;
is_default: boolean;
enabled: boolean;
created_by: string;

View File

@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<OPageLayout
data-test="edit-group-section"
:title="groupDetails.group_name"
:title="raw(groupDetails.group_name)"
:back="{ label: t('iam.groups'), onClick: cancelEditGroup }"
bleed
>
@ -96,7 +96,7 @@ import GroupRoles from "./GroupRoles.vue";
import GroupUsers from "./GroupUsers.vue";
import AppTabs from "@/components/common/AppTabs.vue";
import OPageLayout from "@/lib/core/PageLayout/OPageLayout.vue";
import { useI18nTyped } from "@/types/i18n";
import { raw, useI18nTyped } from "@/types/i18n";
import { useRouter, onBeforeRouteLeave } from "vue-router";
import { onBeforeMount } from "vue";
import { getGroup, updateGroup } from "@/services/iam";

Some files were not shown because too many files have changed in this diff Show More