summaryrefslogtreecommitdiffhomepage
path: root/packages/ui
diff options
context:
space:
mode:
authorFilip <[email protected]>2026-02-13 12:08:13 +0100
committerGitHub <[email protected]>2026-02-13 05:08:13 -0600
commitebb907d646022d2e7bb8effc164e1f09943d64a9 (patch)
tree65662b4834701f2dc1e2e46da7ed5206341f8ba3 /packages/ui
parentb8ee88212639ec63f4fe87555b5e87f74643e76b (diff)
downloadopencode-ebb907d646022d2e7bb8effc164e1f09943d64a9.tar.gz
opencode-ebb907d646022d2e7bb8effc164e1f09943d64a9.zip
fix(desktop): performance optimization for showing large diff & files (#13460)
Diffstat (limited to 'packages/ui')
-rw-r--r--packages/ui/src/components/code.tsx118
-rw-r--r--packages/ui/src/components/diff.tsx38
-rw-r--r--packages/ui/src/components/session-review.css26
-rw-r--r--packages/ui/src/components/session-review.tsx228
-rw-r--r--packages/ui/src/i18n/ar.ts5
-rw-r--r--packages/ui/src/i18n/br.ts5
-rw-r--r--packages/ui/src/i18n/bs.ts5
-rw-r--r--packages/ui/src/i18n/da.ts5
-rw-r--r--packages/ui/src/i18n/de.ts5
-rw-r--r--packages/ui/src/i18n/en.ts5
-rw-r--r--packages/ui/src/i18n/es.ts5
-rw-r--r--packages/ui/src/i18n/fr.ts5
-rw-r--r--packages/ui/src/i18n/ja.ts5
-rw-r--r--packages/ui/src/i18n/ko.ts5
-rw-r--r--packages/ui/src/i18n/no.ts5
-rw-r--r--packages/ui/src/i18n/pl.ts5
-rw-r--r--packages/ui/src/i18n/ru.ts5
-rw-r--r--packages/ui/src/i18n/th.ts5
-rw-r--r--packages/ui/src/i18n/zh.ts5
-rw-r--r--packages/ui/src/i18n/zht.ts5
20 files changed, 370 insertions, 120 deletions
diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx
index abe0d7ca9..837cc5337 100644
--- a/packages/ui/src/components/code.tsx
+++ b/packages/ui/src/components/code.tsx
@@ -1,10 +1,27 @@
-import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs"
+import {
+ DEFAULT_VIRTUAL_FILE_METRICS,
+ type FileContents,
+ File,
+ FileOptions,
+ LineAnnotation,
+ type SelectedLineRange,
+ type VirtualFileMetrics,
+ VirtualizedFile,
+ Virtualizer,
+} from "@pierre/diffs"
import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js"
import { Portal } from "solid-js/web"
import { createDefaultOptions, styleVariables } from "../pierre"
import { getWorkerPool } from "../pierre/worker"
import { Icon } from "./icon"
+const VIRTUALIZE_BYTES = 500_000
+const codeMetrics = {
+ ...DEFAULT_VIRTUAL_FILE_METRICS,
+ lineHeight: 24,
+ fileGap: 0,
+} satisfies Partial<VirtualFileMetrics>
+
type SelectionSide = "additions" | "deletions"
export type CodeProps<T = {}> = FileOptions<T> & {
@@ -160,16 +177,28 @@ export function Code<T>(props: CodeProps<T>) {
const [findPos, setFindPos] = createSignal<{ top: number; right: number }>({ top: 8, right: 8 })
- const file = createMemo(
- () =>
- new File<T>(
- {
- ...createDefaultOptions<T>("unified"),
- ...others,
- },
- getWorkerPool("unified"),
- ),
- )
+ let instance: File<T> | VirtualizedFile<T> | undefined
+ let virtualizer: Virtualizer | undefined
+ let virtualRoot: Document | HTMLElement | undefined
+
+ const bytes = createMemo(() => {
+ const value = local.file.contents as unknown
+ if (typeof value === "string") return value.length
+ if (Array.isArray(value)) {
+ return value.reduce(
+ (acc, part) => acc + (typeof part === "string" ? part.length + 1 : String(part).length + 1),
+ 0,
+ )
+ }
+ if (value == null) return 0
+ return String(value).length
+ })
+ const virtual = createMemo(() => bytes() > VIRTUALIZE_BYTES)
+
+ const options = createMemo(() => ({
+ ...createDefaultOptions<T>("unified"),
+ ...others,
+ }))
const getRoot = () => {
const host = container.querySelector("diffs-container")
@@ -577,6 +606,14 @@ export function Code<T>(props: CodeProps<T>) {
}
const applySelection = (range: SelectedLineRange | null) => {
+ const current = instance
+ if (!current) return false
+
+ if (virtual()) {
+ current.setSelectedLines(range)
+ return true
+ }
+
const root = getRoot()
if (!root) return false
@@ -584,7 +621,7 @@ export function Code<T>(props: CodeProps<T>) {
if (root.querySelectorAll("[data-line]").length < lines) return false
if (!range) {
- file().setSelectedLines(null)
+ current.setSelectedLines(null)
return true
}
@@ -592,12 +629,12 @@ export function Code<T>(props: CodeProps<T>) {
const end = Math.max(range.start, range.end)
if (start < 1 || end > lines) {
- file().setSelectedLines(null)
+ current.setSelectedLines(null)
return true
}
if (!root.querySelector(`[data-line="${start}"]`) || !root.querySelector(`[data-line="${end}"]`)) {
- file().setSelectedLines(null)
+ current.setSelectedLines(null)
return true
}
@@ -608,7 +645,7 @@ export function Code<T>(props: CodeProps<T>) {
return { start: range.start, end: range.end }
})()
- file().setSelectedLines(normalized)
+ current.setSelectedLines(normalized)
return true
}
@@ -619,9 +656,12 @@ export function Code<T>(props: CodeProps<T>) {
const token = renderToken
- const lines = lineCount()
+ const lines = virtual() ? undefined : lineCount()
- const isReady = (root: ShadowRoot) => root.querySelectorAll("[data-line]").length >= lines
+ const isReady = (root: ShadowRoot) =>
+ virtual()
+ ? root.querySelector("[data-line]") != null
+ : root.querySelectorAll("[data-line]").length >= (lines ?? 0)
const notify = () => {
if (token !== renderToken) return
@@ -844,20 +884,41 @@ export function Code<T>(props: CodeProps<T>) {
}
createEffect(() => {
- const current = file()
+ const opts = options()
+ const workerPool = getWorkerPool("unified")
+ const isVirtual = virtual()
- onCleanup(() => {
- current.cleanUp()
- })
- })
-
- createEffect(() => {
observer?.disconnect()
observer = undefined
+ instance?.cleanUp()
+ instance = undefined
+
+ if (!isVirtual && virtualizer) {
+ virtualizer.cleanUp()
+ virtualizer = undefined
+ virtualRoot = undefined
+ }
+
+ const v = (() => {
+ if (!isVirtual) return
+ if (typeof document === "undefined") return
+
+ const root = getScrollParent(wrapper) ?? document
+ if (virtualizer && virtualRoot === root) return virtualizer
+
+ virtualizer?.cleanUp()
+ virtualizer = new Virtualizer()
+ virtualRoot = root
+ virtualizer.setup(root, root instanceof Document ? undefined : wrapper)
+ return virtualizer
+ })()
+
+ instance = isVirtual && v ? new VirtualizedFile<T>(opts, v, codeMetrics, workerPool) : new File<T>(opts, workerPool)
+
container.innerHTML = ""
const value = text()
- file().render({
+ instance.render({
file: typeof local.file.contents === "string" ? local.file : { ...local.file, contents: value },
lineAnnotations: local.annotations,
containerWrapper: container,
@@ -910,6 +971,13 @@ export function Code<T>(props: CodeProps<T>) {
onCleanup(() => {
observer?.disconnect()
+ instance?.cleanUp()
+ instance = undefined
+
+ virtualizer?.cleanUp()
+ virtualizer = undefined
+ virtualRoot = undefined
+
clearOverlayScroll()
clearOverlay()
if (findCurrent === host) {
diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx
index 0966db75e..0002232b0 100644
--- a/packages/ui/src/components/diff.tsx
+++ b/packages/ui/src/components/diff.tsx
@@ -1,5 +1,5 @@
-import { checksum } from "@opencode-ai/util/encode"
-import { FileDiff, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
+import { sampledChecksum } from "@opencode-ai/util/encode"
+import { FileDiff, type FileDiffOptions, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
import { createMediaQuery } from "@solid-primitives/media"
import { createEffect, createMemo, createSignal, onCleanup, splitProps } from "solid-js"
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
@@ -78,14 +78,29 @@ export function Diff<T>(props: DiffProps<T>) {
const mobile = createMediaQuery("(max-width: 640px)")
- const options = createMemo(() => {
- const opts = {
+ const large = createMemo(() => {
+ const before = typeof local.before?.contents === "string" ? local.before.contents : ""
+ const after = typeof local.after?.contents === "string" ? local.after.contents : ""
+ return Math.max(before.length, after.length) > 500_000
+ })
+
+ const largeOptions = {
+ lineDiffType: "none",
+ maxLineDiffLength: 0,
+ tokenizeMaxLineLength: 1,
+ } satisfies Pick<FileDiffOptions<T>, "lineDiffType" | "maxLineDiffLength" | "tokenizeMaxLineLength">
+
+ const options = createMemo<FileDiffOptions<T>>(() => {
+ const base = {
...createDefaultOptions(props.diffStyle),
...others,
}
- if (!mobile()) return opts
+
+ const perf = large() ? { ...base, ...largeOptions } : base
+ if (!mobile()) return perf
+
return {
- ...opts,
+ ...perf,
disableLineNumbers: true,
}
})
@@ -528,12 +543,17 @@ export function Diff<T>(props: DiffProps<T>) {
createEffect(() => {
const opts = options()
- const workerPool = getWorkerPool(props.diffStyle)
+ const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle)
const virtualizer = getVirtualizer()
const annotations = local.annotations
const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : ""
const afterContents = typeof local.after?.contents === "string" ? local.after.contents : ""
+ const cacheKey = (contents: string) => {
+ if (!large()) return sampledChecksum(contents, contents.length)
+ return sampledChecksum(contents)
+ }
+
instance?.cleanUp()
instance = virtualizer
? new VirtualizedFileDiff<T>(opts, virtualizer, virtualMetrics, workerPool)
@@ -545,12 +565,12 @@ export function Diff<T>(props: DiffProps<T>) {
oldFile: {
...local.before,
contents: beforeContents,
- cacheKey: checksum(beforeContents),
+ cacheKey: cacheKey(beforeContents),
},
newFile: {
...local.after,
contents: afterContents,
- cacheKey: checksum(afterContents),
+ cacheKey: cacheKey(afterContents),
},
lineAnnotations: annotations,
containerWrapper: container,
diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css
index 30bfe3b71..46473b75e 100644
--- a/packages/ui/src/components/session-review.css
+++ b/packages/ui/src/components/session-review.css
@@ -222,4 +222,30 @@
--line-comment-popover-z: 30;
--line-comment-open-z: 6;
}
+
+ [data-slot="session-review-large-diff"] {
+ padding: 12px;
+ background: var(--background-stronger);
+ }
+
+ [data-slot="session-review-large-diff-title"] {
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-medium);
+ color: var(--text-strong);
+ margin-bottom: 4px;
+ }
+
+ [data-slot="session-review-large-diff-meta"] {
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ color: var(--text-weak);
+ word-break: break-word;
+ }
+
+ [data-slot="session-review-large-diff-actions"] {
+ display: flex;
+ gap: 8px;
+ margin-top: 10px;
+ }
}
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index fe2475548..5f1e6b1ab 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/packages/ui/src/components/session-review.tsx
@@ -17,6 +17,26 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { type SelectedLineRange } from "@pierre/diffs"
import { Dynamic } from "solid-js/web"
+const MAX_DIFF_LINES = 20_000
+const MAX_DIFF_BYTES = 2_000_000
+
+function linesOver(text: string, max: number) {
+ let lines = 1
+ for (let i = 0; i < text.length; i++) {
+ if (text.charCodeAt(i) !== 10) continue
+ lines++
+ if (lines > max) return true
+ }
+ return lines > max
+}
+
+function formatBytes(bytes: number) {
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${Math.round((bytes / 1024) * 10) / 10} KB`
+ return `${Math.round((bytes / (1024 * 1024)) * 10) / 10} MB`
+}
+
export type SessionReviewDiffStyle = "unified" | "split"
export type SessionReviewComment = {
@@ -326,12 +346,28 @@ export const SessionReview = (props: SessionReviewProps) => {
{(diff) => {
let wrapper: HTMLDivElement | undefined
+ const expanded = createMemo(() => open().includes(diff.file))
+ const [force, setForce] = createSignal(false)
+
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
const commentedLines = createMemo(() => comments().map((c) => c.selection))
const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
const afterText = () => (typeof diff.after === "string" ? diff.after : "")
+ const tooLarge = createMemo(() => {
+ if (!expanded()) return false
+ if (force()) return false
+ if (isImageFile(diff.file)) return false
+
+ const before = beforeText()
+ const after = afterText()
+
+ if (before.length > MAX_DIFF_BYTES || after.length > MAX_DIFF_BYTES) return true
+ if (linesOver(before, MAX_DIFF_LINES) || linesOver(after, MAX_DIFF_LINES)) return true
+ return false
+ })
+
const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
const isDeleted = () =>
diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
@@ -571,94 +607,114 @@ export const SessionReview = (props: SessionReviewProps) => {
scheduleAnchors()
}}
>
- <Switch>
- <Match when={isImage() && imageSrc()}>
- <div data-slot="session-review-image-container">
- <img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
- </div>
- </Match>
- <Match when={isImage() && isDeleted()}>
- <div data-slot="session-review-image-container" data-removed>
- <span data-slot="session-review-image-placeholder">
- {i18n.t("ui.sessionReview.change.removed")}
- </span>
- </div>
- </Match>
- <Match when={isImage() && !imageSrc()}>
- <div data-slot="session-review-image-container">
- <span data-slot="session-review-image-placeholder">
- {imageStatus() === "loading" ? "Loading..." : "Image"}
- </span>
- </div>
- </Match>
- <Match when={!isImage()}>
- <Dynamic
- component={diffComponent}
- preloadedDiff={diff.preloaded}
- diffStyle={diffStyle()}
- onRendered={() => {
- props.onDiffRendered?.()
- scheduleAnchors()
- }}
- enableLineSelection={props.onLineComment != null}
- onLineSelected={handleLineSelected}
- onLineSelectionEnd={handleLineSelectionEnd}
- selectedLines={selectedLines()}
- commentedLines={commentedLines()}
- before={{
- name: diff.file!,
- contents: typeof diff.before === "string" ? diff.before : "",
- }}
- after={{
- name: diff.file!,
- contents: typeof diff.after === "string" ? diff.after : "",
- }}
- />
- </Match>
- </Switch>
-
- <For each={comments()}>
- {(comment) => (
- <LineComment
- id={comment.id}
- top={positions()[comment.id]}
- onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
- onClick={() => {
- if (isCommentOpen(comment)) {
- setOpened(null)
- return
- }
-
- openComment(comment)
- }}
- open={isCommentOpen(comment)}
- comment={comment.comment}
- selection={selectionLabel(comment.selection)}
- />
- )}
- </For>
-
- <Show when={draftRange()}>
- {(range) => (
- <Show when={draftTop() !== undefined}>
- <LineCommentEditor
- top={draftTop()}
- value={draft()}
- selection={selectionLabel(range())}
- onInput={setDraft}
- onCancel={() => setCommenting(null)}
- onSubmit={(comment) => {
- props.onLineComment?.({
- file: diff.file,
- selection: range(),
- comment,
- preview: selectionPreview(diff, range()),
- })
- setCommenting(null)
+ <Show when={expanded()}>
+ <Switch>
+ <Match when={isImage() && imageSrc()}>
+ <div data-slot="session-review-image-container">
+ <img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
+ </div>
+ </Match>
+ <Match when={isImage() && isDeleted()}>
+ <div data-slot="session-review-image-container" data-removed>
+ <span data-slot="session-review-image-placeholder">
+ {i18n.t("ui.sessionReview.change.removed")}
+ </span>
+ </div>
+ </Match>
+ <Match when={isImage() && !imageSrc()}>
+ <div data-slot="session-review-image-container">
+ <span data-slot="session-review-image-placeholder">
+ {imageStatus() === "loading"
+ ? i18n.t("ui.sessionReview.image.loading")
+ : i18n.t("ui.sessionReview.image.placeholder")}
+ </span>
+ </div>
+ </Match>
+ <Match when={!isImage() && tooLarge()}>
+ <div data-slot="session-review-large-diff">
+ <div data-slot="session-review-large-diff-title">
+ {i18n.t("ui.sessionReview.largeDiff.title")}
+ </div>
+ <div data-slot="session-review-large-diff-meta">
+ Limit: {MAX_DIFF_LINES.toLocaleString()} lines / {formatBytes(MAX_DIFF_BYTES)}.
+ Current: {formatBytes(Math.max(beforeText().length, afterText().length))}.
+ </div>
+ <div data-slot="session-review-large-diff-actions">
+ <Button size="normal" variant="secondary" onClick={() => setForce(true)}>
+ {i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
+ </Button>
+ </div>
+ </div>
+ </Match>
+ <Match when={!isImage()}>
+ <Dynamic
+ component={diffComponent}
+ preloadedDiff={diff.preloaded}
+ diffStyle={diffStyle()}
+ onRendered={() => {
+ props.onDiffRendered?.()
+ scheduleAnchors()
+ }}
+ enableLineSelection={props.onLineComment != null}
+ onLineSelected={handleLineSelected}
+ onLineSelectionEnd={handleLineSelectionEnd}
+ selectedLines={selectedLines()}
+ commentedLines={commentedLines()}
+ before={{
+ name: diff.file!,
+ contents: typeof diff.before === "string" ? diff.before : "",
}}
+ after={{
+ name: diff.file!,
+ contents: typeof diff.after === "string" ? diff.after : "",
+ }}
+ />
+ </Match>
+ </Switch>
+
+ <For each={comments()}>
+ {(comment) => (
+ <LineComment
+ id={comment.id}
+ top={positions()[comment.id]}
+ onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
+ onClick={() => {
+ if (isCommentOpen(comment)) {
+ setOpened(null)
+ return
+ }
+
+ openComment(comment)
+ }}
+ open={isCommentOpen(comment)}
+ comment={comment.comment}
+ selection={selectionLabel(comment.selection)}
/>
- </Show>
- )}
+ )}
+ </For>
+
+ <Show when={draftRange()}>
+ {(range) => (
+ <Show when={draftTop() !== undefined}>
+ <LineCommentEditor
+ top={draftTop()}
+ value={draft()}
+ selection={selectionLabel(range())}
+ onInput={setDraft}
+ onCancel={() => setCommenting(null)}
+ onSubmit={(comment) => {
+ props.onLineComment?.({
+ file: diff.file,
+ selection: range(),
+ comment,
+ preview: selectionPreview(diff, range()),
+ })
+ setCommenting(null)
+ }}
+ />
+ </Show>
+ )}
+ </Show>
</Show>
</div>
</Accordion.Content>
diff --git a/packages/ui/src/i18n/ar.ts b/packages/ui/src/i18n/ar.ts
index 7ee17e2e0..9a6c8dcbd 100644
--- a/packages/ui/src/i18n/ar.ts
+++ b/packages/ui/src/i18n/ar.ts
@@ -8,6 +8,11 @@ export const dict = {
"ui.sessionReview.change.added": "مضاف",
"ui.sessionReview.change.removed": "محذوف",
"ui.sessionReview.change.modified": "معدل",
+ "ui.sessionReview.image.loading": "جار التحميل...",
+ "ui.sessionReview.image.placeholder": "صورة",
+ "ui.sessionReview.largeDiff.title": "Diff كبير جدا لعرضه",
+ "ui.sessionReview.largeDiff.meta": "الحد: {{lines}} سطر / {{limit}}. الحالي: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "اعرض على أي حال",
"ui.lineComment.label.prefix": "تعليق على ",
"ui.lineComment.label.suffix": "",
diff --git a/packages/ui/src/i18n/br.ts b/packages/ui/src/i18n/br.ts
index 6d7449d84..148b0ae17 100644
--- a/packages/ui/src/i18n/br.ts
+++ b/packages/ui/src/i18n/br.ts
@@ -8,6 +8,11 @@ export const dict = {
"ui.sessionReview.change.added": "Adicionado",
"ui.sessionReview.change.removed": "Removido",
"ui.sessionReview.change.modified": "Modificado",
+ "ui.sessionReview.image.loading": "Carregando...",
+ "ui.sessionReview.image.placeholder": "Imagem",
+ "ui.sessionReview.largeDiff.title": "Diff grande demais para renderizar",
+ "ui.sessionReview.largeDiff.meta": "Limite: {{lines}} linhas / {{limit}}. Atual: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Renderizar mesmo assim",
"ui.lineComment.label.prefix": "Comentar em ",
"ui.lineComment.label.suffix": "",
diff --git a/packages/ui/src/i18n/bs.ts b/packages/ui/src/i18n/bs.ts
index 24e4c1206..7614af087 100644
--- a/packages/ui/src/i18n/bs.ts
+++ b/packages/ui/src/i18n/bs.ts
@@ -12,6 +12,11 @@ export const dict = {
"ui.sessionReview.change.added": "Dodano",
"ui.sessionReview.change.removed": "Uklonjeno",
"ui.sessionReview.change.modified": "Izmijenjeno",
+ "ui.sessionReview.image.loading": "Učitavanje...",
+ "ui.sessionReview.image.placeholder": "Slika",
+ "ui.sessionReview.largeDiff.title": "Diff je prevelik za prikaz",
+ "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} linija / {{limit}}. Trenutno: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Prikaži svejedno",
"ui.lineComment.label.prefix": "Komentar na ",
"ui.lineComment.label.suffix": "",
diff --git a/packages/ui/src/i18n/da.ts b/packages/ui/src/i18n/da.ts
index 218f3b26a..2f49a9434 100644
--- a/packages/ui/src/i18n/da.ts
+++ b/packages/ui/src/i18n/da.ts
@@ -9,6 +9,11 @@ export const dict = {
"ui.sessionReview.change.added": "Tilføjet",
"ui.sessionReview.change.removed": "Fjernet",
"ui.sessionReview.change.modified": "Ændret",
+ "ui.sessionReview.image.loading": "Indlæser...",
+ "ui.sessionReview.image.placeholder": "Billede",
+ "ui.sessionReview.largeDiff.title": "Diff er for stor til at blive vist",
+ "ui.sessionReview.largeDiff.meta": "Grænse: {{lines}} linjer / {{limit}}. Nuværende: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Vis alligevel",
"ui.lineComment.label.prefix": "Kommenter på ",
"ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "Kommenterer på ",
diff --git a/packages/ui/src/i18n/de.ts b/packages/ui/src/i18n/de.ts
index 921a12c99..44090b7bd 100644
--- a/packages/ui/src/i18n/de.ts
+++ b/packages/ui/src/i18n/de.ts
@@ -13,6 +13,11 @@ export const dict = {
"ui.sessionReview.change.added": "Hinzugefügt",
"ui.sessionReview.change.removed": "Entfernt",
"ui.sessionReview.change.modified": "Geändert",
+ "ui.sessionReview.image.loading": "Wird geladen...",
+ "ui.sessionReview.image.placeholder": "Bild",
+ "ui.sessionReview.largeDiff.title": "Diff zu groß zum Rendern",
+ "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} Zeilen / {{limit}}. Aktuell: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Trotzdem rendern",
"ui.lineComment.label.prefix": "Kommentar zu ",
"ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "Kommentiere ",
diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts
index 631bc660a..9b6ab0bd6 100644
--- a/packages/ui/src/i18n/en.ts
+++ b/packages/ui/src/i18n/en.ts
@@ -8,6 +8,11 @@ export const dict = {
"ui.sessionReview.change.added": "Added",
"ui.sessionReview.change.removed": "Removed",
"ui.sessionReview.change.modified": "Modified",
+ "ui.sessionReview.image.loading": "Loading...",
+ "ui.sessionReview.image.placeholder": "Image",
+ "ui.sessionReview.largeDiff.title": "Diff too large to render",
+ "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} lines / {{limit}}. Current: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Render anyway",
"ui.lineComment.label.prefix": "Comment on ",
"ui.lineComment.label.suffix": "",
diff --git a/packages/ui/src/i18n/es.ts b/packages/ui/src/i18n/es.ts
index 4fd921b60..c2f8ac3b9 100644
--- a/packages/ui/src/i18n/es.ts
+++ b/packages/ui/src/i18n/es.ts
@@ -8,6 +8,11 @@ export const dict = {
"ui.sessionReview.change.added": "Añadido",
"ui.sessionReview.change.removed": "Eliminado",
"ui.sessionReview.change.modified": "Modificado",
+ "ui.sessionReview.image.loading": "Cargando...",
+ "ui.sessionReview.image.placeholder": "Imagen",
+ "ui.sessionReview.largeDiff.title": "Diff demasiado grande para renderizar",
+ "ui.sessionReview.largeDiff.meta": "Límite: {{lines}} líneas / {{limit}}. Actual: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Renderizar de todos modos",
"ui.lineComment.label.prefix": "Comentar en ",
"ui.lineComment.label.suffix": "",
diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts
index 537d01bba..679d56fa7 100644
--- a/packages/ui/src/i18n/fr.ts
+++ b/packages/ui/src/i18n/fr.ts
@@ -8,6 +8,11 @@ export const dict = {
"ui.sessionReview.change.added": "Ajouté",
"ui.sessionReview.change.removed": "Supprimé",
"ui.sessionReview.change.modified": "Modifié",
+ "ui.sessionReview.image.loading": "Chargement...",
+ "ui.sessionReview.image.placeholder": "Image",
+ "ui.sessionReview.largeDiff.title": "Diff trop volumineux pour être affiché",
+ "ui.sessionReview.largeDiff.meta": "Limite : {{lines}} lignes / {{limit}}. Actuel : {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Afficher quand même",
"ui.lineComment.label.prefix": "Commenter sur ",
"ui.lineComment.label.suffix": "",
diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts
index 6086070bd..bf85807d0 100644
--- a/packages/ui/src/i18n/ja.ts
+++ b/packages/ui/src/i18n/ja.ts
@@ -9,6 +9,11 @@ export const dict = {
"ui.sessionReview.change.added": "追加",
"ui.sessionReview.change.removed": "削除",
"ui.sessionReview.change.modified": "変更",
+ "ui.sessionReview.image.loading": "読み込み中...",
+ "ui.sessionReview.image.placeholder": "画像",
+ "ui.sessionReview.largeDiff.title": "差分が大きすぎて表示できません",
+ "ui.sessionReview.largeDiff.meta": "上限: {{lines}} 行 / {{limit}}。現在: {{current}}。",
+ "ui.sessionReview.largeDiff.renderAnyway": "それでも表示する",
"ui.lineComment.label.prefix": "",
"ui.lineComment.label.suffix": "へのコメント",
"ui.lineComment.editorLabel.prefix": "",
diff --git a/packages/ui/src/i18n/ko.ts b/packages/ui/src/i18n/ko.ts
index fd394dbb7..aba793a11 100644
--- a/packages/ui/src/i18n/ko.ts
+++ b/packages/ui/src/i18n/ko.ts
@@ -8,6 +8,11 @@ export const dict = {
"ui.sessionReview.change.added": "추가됨",
"ui.sessionReview.change.removed": "삭제됨",
"ui.sessionReview.change.modified": "수정됨",
+ "ui.sessionReview.image.loading": "로딩 중...",
+ "ui.sessionReview.image.placeholder": "이미지",
+ "ui.sessionReview.largeDiff.title": "차이가 너무 커서 렌더링할 수 없습니다",
+ "ui.sessionReview.largeDiff.meta": "제한: {{lines}}줄 / {{limit}}. 현재: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "그래도 렌더링",
"ui.lineComment.label.prefix": "",
"ui.lineComment.label.suffix": "에 댓글 달기",
diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts
index dcb353614..7982b3ac7 100644
--- a/packages/ui/src/i18n/no.ts
+++ b/packages/ui/src/i18n/no.ts
@@ -11,6 +11,11 @@ export const dict: Record<Keys, string> = {
"ui.sessionReview.change.added": "Lagt til",
"ui.sessionReview.change.removed": "Fjernet",
"ui.sessionReview.change.modified": "Endret",
+ "ui.sessionReview.image.loading": "Laster...",
+ "ui.sessionReview.image.placeholder": "Bilde",
+ "ui.sessionReview.largeDiff.title": "Diff er for stor til å gjengi",
+ "ui.sessionReview.largeDiff.meta": "Grense: {{lines}} linjer / {{limit}}. Nåværende: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Gjengi likevel",
"ui.lineComment.label.prefix": "Kommenter på ",
"ui.lineComment.label.suffix": "",
diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts
index fb10debbb..2489ac7f2 100644
--- a/packages/ui/src/i18n/pl.ts
+++ b/packages/ui/src/i18n/pl.ts
@@ -9,6 +9,11 @@ export const dict = {
"ui.sessionReview.change.added": "Dodano",
"ui.sessionReview.change.removed": "Usunięto",
"ui.sessionReview.change.modified": "Zmodyfikowano",
+ "ui.sessionReview.image.loading": "Ładowanie...",
+ "ui.sessionReview.image.placeholder": "Obraz",
+ "ui.sessionReview.largeDiff.title": "Diff jest zbyt duży, aby go wyrenderować",
+ "ui.sessionReview.largeDiff.meta": "Limit: {{lines}} linii / {{limit}}. Obecnie: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Renderuj mimo to",
"ui.lineComment.label.prefix": "Komentarz do ",
"ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "Komentowanie: ",
diff --git a/packages/ui/src/i18n/ru.ts b/packages/ui/src/i18n/ru.ts
index 417fe0ce8..8e6bb678f 100644
--- a/packages/ui/src/i18n/ru.ts
+++ b/packages/ui/src/i18n/ru.ts
@@ -9,6 +9,11 @@ export const dict = {
"ui.sessionReview.change.added": "Добавлено",
"ui.sessionReview.change.removed": "Удалено",
"ui.sessionReview.change.modified": "Изменено",
+ "ui.sessionReview.image.loading": "Загрузка...",
+ "ui.sessionReview.image.placeholder": "Изображение",
+ "ui.sessionReview.largeDiff.title": "Diff слишком большой для отображения",
+ "ui.sessionReview.largeDiff.meta": "Лимит: {{lines}} строк / {{limit}}. Текущий: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "Отобразить всё равно",
"ui.lineComment.label.prefix": "Комментарий к ",
"ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "Комментирование: ",
diff --git a/packages/ui/src/i18n/th.ts b/packages/ui/src/i18n/th.ts
index 68bb0d733..b036eca2e 100644
--- a/packages/ui/src/i18n/th.ts
+++ b/packages/ui/src/i18n/th.ts
@@ -8,6 +8,11 @@ export const dict = {
"ui.sessionReview.change.added": "เพิ่ม",
"ui.sessionReview.change.removed": "ลบ",
"ui.sessionReview.change.modified": "แก้ไข",
+ "ui.sessionReview.image.loading": "กำลังโหลด...",
+ "ui.sessionReview.image.placeholder": "รูปภาพ",
+ "ui.sessionReview.largeDiff.title": "Diff มีขนาดใหญ่เกินไปจนไม่สามารถแสดงผลได้",
+ "ui.sessionReview.largeDiff.meta": "ขีดจำกัด: {{lines}} บรรทัด / {{limit}}. ปัจจุบัน: {{current}}.",
+ "ui.sessionReview.largeDiff.renderAnyway": "แสดงผลต่อไป",
"ui.lineComment.label.prefix": "แสดงความคิดเห็นบน ",
"ui.lineComment.label.suffix": "",
diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts
index 53beeb1e4..dcb8062a3 100644
--- a/packages/ui/src/i18n/zh.ts
+++ b/packages/ui/src/i18n/zh.ts
@@ -12,6 +12,11 @@ export const dict = {
"ui.sessionReview.change.added": "已添加",
"ui.sessionReview.change.removed": "已移除",
"ui.sessionReview.change.modified": "已修改",
+ "ui.sessionReview.image.loading": "加载中...",
+ "ui.sessionReview.image.placeholder": "图片",
+ "ui.sessionReview.largeDiff.title": "差异过大,无法渲染",
+ "ui.sessionReview.largeDiff.meta": "限制:{{lines}} 行 / {{limit}}。当前:{{current}}。",
+ "ui.sessionReview.largeDiff.renderAnyway": "仍然渲染",
"ui.lineComment.label.prefix": "评论 ",
"ui.lineComment.label.suffix": "",
diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts
index 1449b0530..271a6ded3 100644
--- a/packages/ui/src/i18n/zht.ts
+++ b/packages/ui/src/i18n/zht.ts
@@ -12,6 +12,11 @@ export const dict = {
"ui.sessionReview.change.added": "已新增",
"ui.sessionReview.change.removed": "已移除",
"ui.sessionReview.change.modified": "已修改",
+ "ui.sessionReview.image.loading": "載入中...",
+ "ui.sessionReview.image.placeholder": "圖片",
+ "ui.sessionReview.largeDiff.title": "差異過大,無法渲染",
+ "ui.sessionReview.largeDiff.meta": "限制:{{lines}} 行 / {{limit}}。目前:{{current}}。",
+ "ui.sessionReview.largeDiff.renderAnyway": "仍然渲染",
"ui.lineComment.label.prefix": "評論 ",
"ui.lineComment.label.suffix": "",