diff --git a/webui/package.json b/webui/package.json
index 64f17c0..18043b6 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -39,6 +39,7 @@
"dompurify": "^2.3.4",
"filesize": "^8.0.6",
"history": "^5.1.0",
+ "i18next": "^22.0.2",
"lodash": "^4.17.21",
"lru-cache": "^6.0.0",
"marked": "^4.0.10",
@@ -48,6 +49,7 @@
"sass": "^1.43.4",
"sirv-cli": "^1.0.0",
"sswr": "^1.3.1",
+ "svelte-i18next": "^1.2.2",
"svelte-navigator": "^3.1.5",
"three": "^0.136.0",
"upend": "../tools/upend_js",
diff --git a/webui/src/components/AttributeView.svelte b/webui/src/components/AttributeView.svelte
index 4915d1f..99db197 100644
--- a/webui/src/components/AttributeView.svelte
+++ b/webui/src/components/AttributeView.svelte
@@ -7,6 +7,7 @@
import IconButton from "./utils/IconButton.svelte";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
+ import { i18n } from "../i18n";
export let entries: UpEntry[];
export let type: UpType | undefined = undefined;
@@ -73,7 +74,7 @@
{#if type.name != "HIER"}
{type.label || type.name || "???"}
{:else}
- Members
+ {$i18n.t("Members")}
{/if}
{:else}
diff --git a/webui/src/components/Inspect.svelte b/webui/src/components/Inspect.svelte
index 1343cc4..963fed0 100644
--- a/webui/src/components/Inspect.svelte
+++ b/webui/src/components/Inspect.svelte
@@ -18,6 +18,7 @@
import { deleteEntry, putEntityAttribute, putEntry } from "../lib/api";
import Icon from "./utils/Icon.svelte";
import BlobViewer from "./display/BlobViewer.svelte";
+ import { i18n } from "../i18n";
const dispatch = createEventDispatcher();
const params = useParams();
@@ -188,7 +189,7 @@
}
async function deleteObject() {
- if (confirm(`Really delete "${identities.join(" | ")}"?`)) {
+ if (confirm(`${$i18n.t("Really delete")} "${identities.join(" | ")}"?`)) {
await deleteEntry(address);
dispatch("close");
}
@@ -230,7 +231,7 @@
{#if groups?.length || editable}
-
+
{#each groups as [entryAddress, address]}
@@ -282,7 +283,7 @@
{#if currentUntypedAttributes.length > 0 || editable}
0}
20 MiB) and UpEnd failed to load waveform from server. Generating the waveform locally may slow down your browser. Do you wish to proceed anyway?"
+ $i18n.t(
+ "File is large (>20 MiB) and UpEnd failed to load waveform from server. Generating the waveform locally may slow down your browser. Do you wish to proceed anyway?"
+ )
)
) {
console.warn(
diff --git a/webui/src/components/widgets/EntryList.svelte b/webui/src/components/widgets/EntryList.svelte
index 125ebf5..0033aed 100644
--- a/webui/src/components/widgets/EntryList.svelte
+++ b/webui/src/components/widgets/EntryList.svelte
@@ -15,6 +15,7 @@
import { defaultEntitySort, entityValueSort } from "../../util/sort";
import { attributeLabels } from "../../util/labels";
import { formatDuration } from "../../util/fragments/time";
+ import { i18n } from "../../i18n";
const dispatch = createEventDispatcher();
export let columns: string | undefined = undefined;
@@ -49,7 +50,7 @@
newEntryValue = undefined;
}
async function removeEntry(address: string) {
- if (confirm("Are you sure you want to remove the attribute?")) {
+ if (confirm($i18n.t("Are you sure you want to remove the attribute?"))) {
dispatch("change", { type: "delete", address } as AttributeChange);
}
}
@@ -144,9 +145,9 @@
// Formatting & Display
const COLUMN_LABELS: { [key: string]: string } = {
- entity: "Entity",
- attribute: "Attribute",
- value: "Value",
+ entity: $i18n.t("Entity"),
+ attribute: $i18n.t("Attribute"),
+ value: $i18n.t("Value"),
};
function formatValue(value: string | number, attribute: string): string {
diff --git a/webui/src/i18n/en.json b/webui/src/i18n/en.json
new file mode 100644
index 0000000..26f9b80
--- /dev/null
+++ b/webui/src/i18n/en.json
@@ -0,0 +1,14 @@
+{
+ "attributes": {
+ "FILE_MIME": "MIME type",
+ "FILE_MTIME": "Last modified",
+ "FILE_SIZE": "File size",
+ "ADDED": "Added at",
+ "LAST_VISITED": "Last visited at",
+ "NUM_VISITED": "Times visited",
+ "LBL": "Label",
+ "IS": "Type",
+ "TYPE": "Type ID",
+ "MEDIA_DURATION": "Duration"
+ }
+}
diff --git a/webui/src/i18n/index.ts b/webui/src/i18n/index.ts
new file mode 100644
index 0000000..650b200
--- /dev/null
+++ b/webui/src/i18n/index.ts
@@ -0,0 +1,12 @@
+import i18next from "i18next";
+import { createI18nStore } from "svelte-i18next";
+import en from "./en.json";
+
+i18next.init({
+ lng: "en",
+ resources: {
+ en,
+ },
+});
+
+export const i18n = createI18nStore(i18next);
diff --git a/webui/src/util/labels.ts b/webui/src/util/labels.ts
index b0861cb..1da62c2 100644
--- a/webui/src/util/labels.ts
+++ b/webui/src/util/labels.ts
@@ -1,23 +1,11 @@
-import { readable, type Readable } from "svelte/store";
+import { i18n } from "../i18n";
+import { derived, readable, type Readable } from "svelte/store";
import { fetchAllAttributes } from "../lib/api";
-const DEFAULT_ATTRIBUTE_LABELS = {
- FILE_MIME: "MIME type",
- FILE_MTIME: "Last modified",
- FILE_SIZE: "File size",
- ADDED: "Added at",
- LAST_VISITED: "Last visited at",
- NUM_VISITED: "Times visited",
- LBL: "Label",
- IS: "Type",
- TYPE: "Type ID",
- MEDIA_DURATION: "Duration",
-};
-
-export const attributeLabels: Readable<{ [key: string]: string }> = readable(
- DEFAULT_ATTRIBUTE_LABELS,
+const databaseAttributeLabels: Readable<{ [key: string]: string }> = readable(
+ {},
(set) => {
- const result = Object.assign(DEFAULT_ATTRIBUTE_LABELS);
+ const result = {};
fetchAllAttributes().then((attributes) => {
attributes.forEach((attribute) => {
if (attribute.labels.length) {
@@ -28,3 +16,13 @@ export const attributeLabels: Readable<{ [key: string]: string }> = readable(
});
}
);
+
+export const attributeLabels: Readable<{ [key: string]: string }> = derived(
+ [i18n, databaseAttributeLabels],
+ ([i18n, attributeLabels]) => {
+ const result = {};
+ Object.assign(result, i18n.getResourceBundle(i18n.language, "attributes"));
+ Object.assign(result, attributeLabels);
+ return result;
+ }
+);
diff --git a/webui/src/views/Home.svelte b/webui/src/views/Home.svelte
index 9e27d78..aef2afb 100644
--- a/webui/src/views/Home.svelte
+++ b/webui/src/views/Home.svelte
@@ -11,6 +11,7 @@
import { query } from "../lib/entity";
import { vaultInfo } from "../util/info";
import { updateTitle } from "../util/title";
+ import { i18n } from "../i18n";
const roots = (async () => {
const data = await fetchRoots();
@@ -114,7 +115,7 @@
- Roots
+ {$i18n.t("Roots")}
{#await roots}
{:then data}
@@ -133,7 +134,7 @@
{#if frequent.length || $frequentQuery === undefined}
- Frequently visited
+ {$i18n.t("Frequently visited")}
{#if $frequentQuery == undefined}
{:else}
@@ -147,7 +148,7 @@
{/if}
{#if recent.length || $recentQuery === undefined}
- Recently visited
+ {$i18n.t("Recently visited")}
{#if $recentQuery == undefined}
{:else}
@@ -163,7 +164,7 @@
{#if latest.length || $latestQuery === undefined}
- Most recently added
+ {$i18n.t("Most recently added")}
{#if $latestQuery == undefined}
{:else}
@@ -177,13 +178,14 @@
{/if}
- View store statistics
+ {$i18n.t("View store statistics")}