diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts
index 0c44e05dad1..a1955223d44 100644
--- a/packages/app/src/runtime/i18n/en.ts
+++ b/packages/app/src/runtime/i18n/en.ts
@@ -285,6 +285,7 @@ export const dict = {
"provider.custom.description.prefix": "Configure an OpenAI-compatible provider. See the ",
"provider.custom.description.link": "provider config docs",
"provider.custom.description.suffix": ".",
+ "provider.custom.description": "Configure an OpenAI-compatible provider. See the {{link}}.",
"provider.custom.field.providerID.label": "Provider ID",
"provider.custom.field.providerID.placeholder": "myprovider",
"provider.custom.field.providerID.description": "Lowercase letters, numbers, hyphens, or underscores",
@@ -707,6 +708,8 @@ export const dict = {
"file.view.preview": "Preview",
"file.view.source": "Source",
"file.view.openInBrowser": "Open in browser",
+ "file.view.videoLabel": "Video: {{filename}}",
+ "file.view.audioLabel": "Audio: {{filename}}",
"file.view.binary": "Binary file · {{size}}",
"file.view.table.rows.one": "{{count}} row",
"file.view.table.rows.other": "{{count}} rows",
diff --git a/packages/app/src/runtime/i18n/language.tsx b/packages/app/src/runtime/i18n/language.tsx
index 5805dad4ab5..2ec54cfbeb5 100644
--- a/packages/app/src/runtime/i18n/language.tsx
+++ b/packages/app/src/runtime/i18n/language.tsx
@@ -319,6 +319,7 @@ export function UiI18nBridge(props: { children?: JSX.Element }) {
t: language.t as UiI18n["t"],
plural: language.plural,
pluralForm: language.pluralForm,
+ rich: language.rich as UiI18n["rich"],
}}
>
{props.children}
diff --git a/packages/app/src/servers/connect/dialog.tsx b/packages/app/src/servers/connect/dialog.tsx
index 4324da29fd5..16635fdcb08 100644
--- a/packages/app/src/servers/connect/dialog.tsx
+++ b/packages/app/src/servers/connect/dialog.tsx
@@ -107,8 +107,11 @@ export const DialogServer: Component<{
>
-
+
-
+
-
+
props.content)
return (
@@ -274,6 +275,7 @@ function ArtifactVideo(props: MediaProps) {
data-slot="artifact-media"
class="w-full bg-black"
controls
+ aria-label={language.t("file.view.videoLabel", { filename: getFilename(props.path) })}
preload="metadata"
playsinline
onError={() => props.onError()}
@@ -289,6 +291,7 @@ function ArtifactVideo(props: MediaProps) {
}
function ArtifactAudio(props: MediaProps) {
+ const language = useLanguage()
const url = createBlobUrl(() => props.content)
return (
@@ -302,6 +305,7 @@ function ArtifactAudio(props: MediaProps) {
class="w-full"
onError={() => props.onError()}
controls
+ aria-label={language.t("file.view.audioLabel", { filename: getFilename(props.path) })}
preload="metadata"
src={url()}
onLoadedMetadata={(event) => props.onInfo({ duration: event.currentTarget.duration })}
diff --git a/packages/session-ui/src/components/file-media.tsx b/packages/session-ui/src/components/file-media.tsx
index 216873f1747..145b84c158c 100644
--- a/packages/session-ui/src/components/file-media.tsx
+++ b/packages/session-ui/src/components/file-media.tsx
@@ -240,7 +240,13 @@ export function FileMedia(props: { media?: FileMediaOptions; fallback: () => JSX
return (
diff --git a/packages/session-ui/src/components/line-comment.tsx b/packages/session-ui/src/components/line-comment.tsx
index 6f4206770e0..eec565638e5 100644
--- a/packages/session-ui/src/components/line-comment.tsx
+++ b/packages/session-ui/src/components/line-comment.tsx
@@ -146,11 +146,7 @@ export const LineComment = (props: LineCommentProps) => {
{split.actions}
-
- {i18n.t("ui.lineComment.label.prefix")}
- {split.selection}
- {i18n.t("ui.lineComment.label.suffix")}
-
+
{i18n.rich("ui.lineComment.label", { selection: split.selection })}
)
@@ -378,9 +374,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
- {i18n.t("ui.lineComment.editorLabel.prefix")}
- {split.selection}
- {i18n.t("ui.lineComment.editorLabel.suffix")}
+ {i18n.rich("ui.lineComment.editorLabel", { selection: split.selection })}
diff --git a/packages/session-ui/src/tools/tool-renderer.tsx b/packages/session-ui/src/tools/tool-renderer.tsx
index ba3f9084406..66bb1045443 100644
--- a/packages/session-ui/src/tools/tool-renderer.tsx
+++ b/packages/session-ui/src/tools/tool-renderer.tsx
@@ -1032,7 +1032,7 @@ function FileTool(props: ToolProps & { title: string; count: number; children?:
rail={false}
trigger={{
title: props.title,
- subtitle: props.count > 0 ? `${props.count} ${i18n.plural("ui.common.file", props.count)}` : "",
+ subtitle: props.count > 0 ? i18n.plural("ui.common.fileCount", props.count) : "",
}}
>
{props.children}
@@ -1273,12 +1273,12 @@ function toolErrorSubtitle(props: ToolProps, i18n: UiI18n) {
Array.isArray(props.metadata.files) ? props.metadata.files.filter(changedFileDiff).map((file) => file.file) : [],
).size
if (count === 0) return undefined
- return `${count} ${i18n.plural("ui.common.file", count)}`
+ return i18n.plural("ui.common.fileCount", count)
}
if (props.tool === "question") {
const count = Array.isArray(props.input.questions) ? props.input.questions.filter(questionInfo).length : 0
if (count === 0) return undefined
- return `${count} ${i18n.plural("ui.common.question", count)}`
+ return i18n.plural("ui.common.questionCount", count)
}
return undefined
}
@@ -2129,8 +2129,8 @@ ToolRegistry.register({
const subtitle = createMemo(() => {
const count = questions().length
if (count === 0) return ""
- if (completed()) return i18n.t("ui.question.subtitle.answered", { count })
- return `${count} ${i18n.plural("ui.common.question", count)}`
+ if (completed()) return i18n.plural("ui.question.subtitle.answeredCount", count)
+ return i18n.plural("ui.common.questionCount", count)
})
return (
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
index fc67346bfa5..d2c776b3c6c 100644
--- a/packages/ui/src/components/list.tsx
+++ b/packages/ui/src/components/list.tsx
@@ -247,16 +247,9 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void })
const query = filter()
if (!query) return i18n.t("ui.list.empty")
- const suffix = i18n.t("ui.list.emptyWithFilter.suffix")
- return (
- <>
- {i18n.t("ui.list.emptyWithFilter.prefix")}
- "{query}"
-
- {suffix}
-
- >
- )
+ return i18n.rich("ui.list.emptyWithFilter", {
+ query: "{query}",
+ })
}
return (
diff --git a/packages/ui/src/context/i18n.test.ts b/packages/ui/src/context/i18n.test.ts
index d3c86eea40e..eac9118c190 100644
--- a/packages/ui/src/context/i18n.test.ts
+++ b/packages/ui/src/context/i18n.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
-import { createUiI18n, localizedListSeparator, pluralCategory, type UiI18nSource } from "./i18n"
+import { createUiI18n, localizedListSeparator, pluralCategory, richTemplateParts, type UiI18nSource } from "./i18n"
const i18n = (locale: string, translated: string) => {
const source: UiI18nSource = {
@@ -62,3 +62,11 @@ describe("localizedListSeparator", () => {
expect(i18n("de", "").list(["Lesen", "Suchen"])).toBe("Lesen und Suchen")
})
})
+
+describe("richTemplateParts", () => {
+ test("lets each locale position a rich slot", () => {
+ const selection = "SELECTION"
+ expect(richTemplateParts("Comment on {{selection}}", { selection })).toEqual(["Comment on ", selection])
+ expect(richTemplateParts("{{selection}}에 댓글", { selection })).toEqual([selection, "에 댓글"])
+ })
+})
diff --git a/packages/ui/src/context/i18n.tsx b/packages/ui/src/context/i18n.tsx
index bcbf3960a42..380bf547b79 100644
--- a/packages/ui/src/context/i18n.tsx
+++ b/packages/ui/src/context/i18n.tsx
@@ -1,4 +1,4 @@
-import { createContext, useContext, type Accessor, type ParentProps } from "solid-js"
+import { createContext, useContext, type Accessor, type JSX, type ParentProps } from "solid-js"
import { I18nProvider } from "@kobalte/core/i18n"
import { dict as en } from "../i18n/en"
import type { Key, LocaleKey, PluralCategory, PluralKey, PluralLookupKey } from "../i18n/en"
@@ -20,6 +20,7 @@ export type UiI18nSource = {
t: UiTranslate
plural: (key: UiI18nPluralKey, count: number, params?: UiI18nParams) => string
pluralForm?: (key: UiI18nPluralKey, category: UiPluralCategory, params?: UiI18nParams) => string
+ rich?: (key: UiI18nOrdinaryKey, params: Record) => JSX.Element[]
}
export type UiI18n = UiI18nSource & {
@@ -27,6 +28,7 @@ export type UiI18n = UiI18nSource & {
tDynamic: (key: UiI18nOrdinaryKey, source: string, params?: UiI18nParams) => string
list: (items: readonly string[]) => string
listSeparator: (index: number, count: number) => string
+ rich: (key: UiI18nOrdinaryKey, params: Record) => JSX.Element[]
}
const rules = new Map()
@@ -68,6 +70,17 @@ function resolveTemplate(text: string, params?: UiI18nParams) {
})
}
+export function richTemplateParts(template: string, params: Record) {
+ return template
+ .split(/({{\s*[^}]+?\s*}})/g)
+ .filter(Boolean)
+ .map((part) => {
+ const match = part.match(/^{{\s*([^}]+?)\s*}}$/)
+ if (!match) return part
+ return params[match[1]] ?? ""
+ })
+}
+
export function createUiI18n(source: UiI18nSource): UiI18n {
return {
...source,
@@ -84,6 +97,7 @@ export function createUiI18n(source: UiI18nSource): UiI18n {
? ","
: ""
: localizedListSeparator(source.locale(), index, count),
+ rich: source.rich ?? ((key, params) => richTemplateParts(source.t(key), params)),
}
}
diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts
index f5f68b39d19..1c585975ab6 100644
--- a/packages/ui/src/i18n/en.ts
+++ b/packages/ui/src/i18n/en.ts
@@ -50,6 +50,8 @@ const source = {
"ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "Commenting on ",
"ui.lineComment.editorLabel.suffix": "",
+ "ui.lineComment.label": "Comment on {{selection}}",
+ "ui.lineComment.editorLabel": "Commenting on {{selection}}",
"ui.lineComment.placeholder": "Add comment",
"ui.lineComment.contextPlaceholder": "Add context for this change",
"ui.lineComment.submit": "Comment",
@@ -135,6 +137,7 @@ const source = {
"ui.list.clearFilter": "Clear filter",
"ui.list.emptyWithFilter.prefix": "No results for",
"ui.list.emptyWithFilter.suffix": "",
+ "ui.list.emptyWithFilter": "No results for {{query}}",
"ui.fileSearch.placeholder": "Find",
"ui.fileSearch.previousMatch": "Previous match",
@@ -217,6 +220,8 @@ const source = {
"ui.common.fileCount.other": "{{count}} files",
"ui.common.question.one": "question",
"ui.common.question.other": "questions",
+ "ui.common.questionCount.one": "{{count}} question",
+ "ui.common.questionCount.other": "{{count}} questions",
"ui.common.add": "Add",
"ui.common.clear": "Clear",
@@ -267,6 +272,9 @@ const source = {
"ui.patch.action.patched": "Patched",
"ui.question.subtitle.answered": "{{count}} answered",
+ "ui.question.subtitle.answeredCount.one": "{{count}} answered",
+ "ui.question.subtitle.answeredCount.other": "{{count}} answered",
+ "ui.fileMedia.audioLabel": "Audio: {{filename}}",
"ui.question.answer.none": "(no answer)",
"ui.question.review.notAnswered": "(not answered)",
"ui.question.multiHint": "Select all answers that apply",