From 97d381e9938f4ad0cb2c62fe6c56ed92c73fd193 Mon Sep 17 00:00:00 2001
From: wukko <me@wukko.me>
Date: Wed, 3 Jul 2024 23:54:44 +0600
Subject: [PATCH] web: move all strings to i18n & improve a11y

- omnibox is now fully usable with a screen reader
- back button is now interpreted as such
- subtext now accepts line breaks
---
 web/i18n/en/a11y/general.json                 |  3 +
 web/i18n/en/a11y/save.json                    |  8 +-
 web/i18n/en/general.json                      |  3 +
 web/i18n/en/save.json                         |  3 +-
 web/i18n/en/settings.json                     | 82 +++++++++++++++++++
 web/i18n/ru/a11y/general.json                 |  3 +
 web/i18n/ru/a11y/save.json                    |  8 +-
 web/i18n/ru/general.json                      |  3 +
 web/i18n/ru/save.json                         |  3 +-
 .../save/buttons/ClearButton.svelte           |  6 +-
 .../save/buttons/DownloadButton.svelte        | 47 +++++++----
 .../settings/SettingsSection.svelte           |  4 +-
 .../components/settings/SettingsTab.svelte    |  4 +-
 web/src/components/sidebar/CobaltLogo.svelte  |  1 +
 web/src/lib/i18n/translations.ts              | 35 ++++++++
 web/src/routes/+layout.svelte                 |  1 +
 web/src/routes/+page.svelte                   |  6 +-
 web/src/routes/about/+page.svelte             |  4 +-
 web/src/routes/donate/+page.svelte            |  4 +-
 web/src/routes/settings/+layout.svelte        | 21 +++--
 .../settings/general/appearance/+page.svelte  | 34 +++++---
 .../routes/settings/save/audio/+page.svelte   | 26 +++---
 .../settings/save/metadata/+page.svelte       | 24 +++---
 .../routes/settings/save/video/+page.svelte   | 35 ++++----
 web/src/routes/updates/+page.svelte           |  4 +-
 25 files changed, 282 insertions(+), 90 deletions(-)
 create mode 100644 web/i18n/en/a11y/general.json
 create mode 100644 web/i18n/en/general.json
 create mode 100644 web/i18n/en/settings.json
 create mode 100644 web/i18n/ru/a11y/general.json
 create mode 100644 web/i18n/ru/general.json

diff --git a/web/i18n/en/a11y/general.json b/web/i18n/en/a11y/general.json
new file mode 100644
index 00000000..30c862e1
--- /dev/null
+++ b/web/i18n/en/a11y/general.json
@@ -0,0 +1,3 @@
+{
+    "back": "go back"
+}
diff --git a/web/i18n/en/a11y/save.json b/web/i18n/en/a11y/save.json
index 93f66bbc..25acced3 100644
--- a/web/i18n/en/a11y/save.json
+++ b/web/i18n/en/a11y/save.json
@@ -1,3 +1,9 @@
 {
-    "linkArea": "link input area"
+    "linkArea": "link input area",
+    "clearInput": "clear input",
+    "download": "download",
+    "downloadThink": "processing the link...",
+    "downloadCheck": "verifying download...",
+    "downloadDone": "downloading done",
+    "downloadError": "downloading error"
 }
diff --git a/web/i18n/en/general.json b/web/i18n/en/general.json
new file mode 100644
index 00000000..6ea5e8a0
--- /dev/null
+++ b/web/i18n/en/general.json
@@ -0,0 +1,3 @@
+{
+    "cobalt": "cobalt"
+}
diff --git a/web/i18n/en/save.json b/web/i18n/en/save.json
index f3b6e6c5..8e87f514 100644
--- a/web/i18n/en/save.json
+++ b/web/i18n/en/save.json
@@ -4,5 +4,6 @@
     "auto": "auto",
     "audio": "audio",
     "mute": "mute",
-    "inputPlaceholder": "paste the link here"
+    "inputPlaceholder": "paste the link here",
+    "termsNote": "by continuing you agree to terms and ethics of use"
 }
diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json
new file mode 100644
index 00000000..08f31a7e
--- /dev/null
+++ b/web/i18n/en/settings.json
@@ -0,0 +1,82 @@
+{
+    "page.appearance": "appearance",
+    "page.video": "video",
+    "page.audio": "audio",
+    "page.metadata": "metadata",
+
+    "section.general": "general",
+    "section.save": "save",
+
+    "theme": "theme",
+    "theme.auto": "auto",
+    "theme.light": "light",
+    "theme.dark": "dark",
+    "theme.description": "auto theme automatically switches between light and dark themes depending on your device's current theme.",
+
+    "video.quality": "preferred video quality",
+    "video.quality.max": "8k+",
+    "video.quality.2160": "4k",
+    "video.quality.1440": "1440p",
+    "video.quality.1080": "1080p",
+    "video.quality.720": "720p",
+    "video.quality.480": "480p",
+    "video.quality.360": "360p",
+    "video.quality.240": "240p",
+    "video.quality.144": "144p",
+    "video.quality.description": "if preferred video quality isn't available, closest best is picked instead.",
+
+    "video.youtube.codec": "preferred youtube video codec",
+    "video.youtube.codec.h264": "h264 (mp4)",
+    "video.youtube.codec.av1": "av1 (mp4)",
+    "video.youtube.codec.vp9": "vp9 (webm)",
+    "video.youtube.codec.description": "if preferred codec isn’t available, next best is picked instead. \n\nh264: best support, average detail level. max quality is 1080p. \nav1: best quality, small file size, most detail. supports 8k & HDR. \nvp9: same quality as av1, but file is approximately two times bigger. supports 4k & HDR.",
+
+    "video.twitter.gif": "twitter/x",
+    "video.twitter.gif.title": "convert looping videos to GIF",
+    "video.twitter.gif.description": "GIF conversion is very inefficient, converted file may be obnoxiously big and low quality.",
+
+    "video.tiktok.h265": "tiktok",
+    "video.tiktok.h265.title": "prefer HEVC/H265 format",
+    "video.tiktok.h265.description": "allows 1080p video downloading at cost of compatibility.",
+
+    "audio.format": "preferred audio format",
+    "audio.format.best": "best",
+    "audio.format.mp3": "mp3",
+    "audio.format.ogg": "ogg",
+    "audio.format.wav": "wav",
+    "audio.format.opus": "opus",
+    "audio.format.description": "every format but \"best\" is converted, meaning that they're lossy. if preferred format matches best available audio, it won't be converted.",
+
+    "audio.youtube.dub": "youtube",
+    "audio.youtube.dub.title": "use browser language for dubbed videos",
+    "audio.youtube.dub.description": "works even if cobalt isn't translated to your language.",
+
+    "audio.tiktok.original": "tiktok",
+    "audio.tiktok.original.title": "use original sound",
+    "audio.tiktok.original.description": "downloads original sound used in the post without any additional changes by the post's author.",
+
+    "metadata.filename": "filename style",
+    "metadata.filename.classic": "classic",
+    "metadata.filename.basic": "basic",
+    "metadata.filename.pretty": "pretty",
+    "metadata.filename.nerdy": "nerdy",
+    "metadata.filename.description": "filename style using which cobalt files will be downloaded. this description is temporary as there's no dynamic preview component yet.",
+
+    "metadata.file": "file metadata",
+    "metadata.disable.title": "disable file metadata",
+    "metadata.disable.description": "title, artist, and other info will not be added to the file.",
+
+    "saving.method": "saving method",
+    "saving.ask.title": "ask how to save",
+    "saving.ask.description": "offer you several ways to save the file instead of opening it in a new tab.",
+
+    "accessibility": "accessibility",
+    "accessibility.transparency.title": "reduce visual transparency",
+    "accessibility.transparency.description": "reduces transparency of surfaces and disables blur effects.",
+    "accessibility.animations.title": "reduce animations",
+    "accessibility.animations.description": "replaces rapid animations with smooth transitions when possible.",
+
+    "language": "language",
+    "language.auto.title": "use default browser language",
+    "language.auto.description": "automatically picks the best language for you. if preferred browser language isn't available, english is used instead."
+}
diff --git a/web/i18n/ru/a11y/general.json b/web/i18n/ru/a11y/general.json
new file mode 100644
index 00000000..64053ece
--- /dev/null
+++ b/web/i18n/ru/a11y/general.json
@@ -0,0 +1,3 @@
+{
+    "back": "назад"
+}
diff --git a/web/i18n/ru/a11y/save.json b/web/i18n/ru/a11y/save.json
index 0123401a..387e490a 100644
--- a/web/i18n/ru/a11y/save.json
+++ b/web/i18n/ru/a11y/save.json
@@ -1,3 +1,9 @@
 {
-    "linkArea": "зона вставки ссылки"
+    "linkArea": "зона вставки ссылки",
+    "clearInput": "clear input",
+    "download": "скачать",
+    "downloadThink": "обрабатываю ссылку...",
+    "downloadCheck": "проверяю загрузку...",
+    "downloadDone": "загрузка завершена!",
+    "downloadError": "ошибка загрузки"
 }
diff --git a/web/i18n/ru/general.json b/web/i18n/ru/general.json
new file mode 100644
index 00000000..75ab3b18
--- /dev/null
+++ b/web/i18n/ru/general.json
@@ -0,0 +1,3 @@
+{
+    "cobalt": "кобальт"
+}
diff --git a/web/i18n/ru/save.json b/web/i18n/ru/save.json
index f751a63d..0093129e 100644
--- a/web/i18n/ru/save.json
+++ b/web/i18n/ru/save.json
@@ -4,5 +4,6 @@
     "auto": "авто",
     "audio": "аудио",
     "mute": "без звука",
-    "inputPlaceholder": "вставь ссылку сюда"
+    "inputPlaceholder": "вставь ссылку сюда",
+    "termsNote": "продолжая, ты соглашаешься с условиями и этикой использования"
 }
diff --git a/web/src/components/save/buttons/ClearButton.svelte b/web/src/components/save/buttons/ClearButton.svelte
index 76f20d3e..9962aa1b 100644
--- a/web/src/components/save/buttons/ClearButton.svelte
+++ b/web/src/components/save/buttons/ClearButton.svelte
@@ -1,9 +1,11 @@
 <script>
-    export let click;
+    import { t } from "$lib/i18n/translations";
     import IconX from '@tabler/icons-svelte/IconX.svelte';
+
+    export let click;
 </script>
 
-<button id="clear-button" on:click={click}>
+<button id="clear-button" on:click={click} aria-label={$t("a11y.save.clearInput")}>
     <IconX color="var(--secondary)" size="16px"/>
 </button>
 
diff --git a/web/src/components/save/buttons/DownloadButton.svelte b/web/src/components/save/buttons/DownloadButton.svelte
index da01fe4a..96510462 100644
--- a/web/src/components/save/buttons/DownloadButton.svelte
+++ b/web/src/components/save/buttons/DownloadButton.svelte
@@ -1,46 +1,53 @@
 <script lang="ts">
-    import '@fontsource-variable/noto-sans-mono';
+    import "@fontsource-variable/noto-sans-mono";
 
     import API from "$lib/api";
-    import { device } from '$lib/device';
+    import { device } from "$lib/device";
+    import { t } from "$lib/i18n/translations";
 
     export let url: string;
 
-    $: buttonText = '>>';
+    $: buttonText = ">>";
+    $: buttonAltText = $t('a11y.save.download');
     $: isDisabled = false;
 
     const changeDownloadButton = (state: string) => {
         isDisabled = true;
-        switch(state) {
+        switch (state) {
             case "think":
-                buttonText = '...';
+                buttonText = "...";
+                buttonAltText = $t('a11y.save.downloadThink');
                 break;
             case "check":
-                buttonText = '..?';
+                buttonText = "..?";
+                buttonAltText = $t('a11y.save.downloadCheck');
                 break;
             case "done":
-                buttonText = '>>>';
+                buttonText = ">>>";
+                buttonAltText = $t('a11y.save.downloadDone');
                 break;
             case "error":
-                buttonText = '!!';
+                buttonText = "!!";
+                buttonAltText = $t('a11y.save.downloadError');
                 break;
         }
-    }
+    };
 
     const restoreDownloadButton = () => {
         setTimeout(() => {
-            buttonText = '>>';
+            buttonText = ">>";
             isDisabled = false;
-        }, 2500)
-    }
+            buttonAltText = $t('a11y.save.download');
+        }, 2500);
+    };
 
     const downloadFile = (url: string) => {
         if (device.is.iOS) {
             return navigator?.share({ url }).catch(() => {});
         } else {
-            return window.open(url, '_blank');
+            return window.open(url, "_blank");
         }
-    }
+    };
 
     // alerts are temporary, we don't have an error popup yet >_<
     export const download = async (link: string) => {
@@ -52,7 +59,7 @@
             changeDownloadButton("error");
             restoreDownloadButton();
 
-            return alert("couldn't access the api")
+            return alert("couldn't access the api");
         }
 
         if (response.status === "error" || response.status === "rate-limit") {
@@ -89,7 +96,12 @@
     };
 </script>
 
-<button id="download-button" disabled={isDisabled} on:click={() => download(url)}>
+<button
+    id="download-button"
+    disabled={isDisabled}
+    on:click={() => download(url)}
+    aria-label={buttonAltText}
+>
     <span id="download-state">{buttonText}</span>
 </button>
 
@@ -120,7 +132,8 @@
 
     #download-state {
         font-size: 24px;
-        font-family: "Noto Sans Mono Variable", "Noto Sans Mono", "IBM Plex Mono", monospace;
+        font-family: "Noto Sans Mono Variable", "Noto Sans Mono",
+            "IBM Plex Mono", monospace;
         font-weight: 400;
 
         text-align: center;
diff --git a/web/src/components/settings/SettingsSection.svelte b/web/src/components/settings/SettingsSection.svelte
index cab74bb0..0ee02d60 100644
--- a/web/src/components/settings/SettingsSection.svelte
+++ b/web/src/components/settings/SettingsSection.svelte
@@ -1,9 +1,11 @@
 <script lang="ts">
+    import { t } from "$lib/i18n/translations";
+
     export let sectionTitle: string;
 </script>
 
 <section id="settings-section">
-    <div id="settings-section-title">{sectionTitle}</div>
+    <div id="settings-section-title">{$t(`settings.section.${sectionTitle}`)}</div>
     <div id="settings-section-categories">
         <slot></slot>
     </div>
diff --git a/web/src/components/settings/SettingsTab.svelte b/web/src/components/settings/SettingsTab.svelte
index 12ebbb06..b16002d7 100644
--- a/web/src/components/settings/SettingsTab.svelte
+++ b/web/src/components/settings/SettingsTab.svelte
@@ -1,6 +1,8 @@
 <script lang="ts">
     import { page } from "$app/stores";
 
+    import { t } from "$lib/i18n/translations";
+
     import IconChevronRight from "@tabler/icons-svelte/IconChevronRight.svelte";
 
     export let tabName: string;
@@ -20,7 +22,7 @@
         <div class="tab-icon" style="background: var(--{iconColor})">
             <slot></slot>
         </div>
-        <span>{tabName}</span>
+        <span>{$t(`settings.page.${tabName}`)}</span>
     </div>
     <div class="settings-tab-chevron">
         <IconChevronRight />
diff --git a/web/src/components/sidebar/CobaltLogo.svelte b/web/src/components/sidebar/CobaltLogo.svelte
index 891da1da..1de5af2e 100644
--- a/web/src/components/sidebar/CobaltLogo.svelte
+++ b/web/src/components/sidebar/CobaltLogo.svelte
@@ -13,6 +13,7 @@
         align-items: center;
         padding: calc(var(--padding) * 2 - 2px);
     }
+
     @media screen and (max-width: 535px) {
         #cobalt-logo {
             display: none;
diff --git a/web/src/lib/i18n/translations.ts b/web/src/lib/i18n/translations.ts
index 03814a1b..8ddac2d4 100644
--- a/web/src/lib/i18n/translations.ts
+++ b/web/src/lib/i18n/translations.ts
@@ -46,6 +46,27 @@ export const config: Config = {
                 await import(`$i18n/en/a11y/meowbalt.json`)
             ).default,
         },
+        {
+            locale: 'en',
+            key: 'settings',
+            loader: async () => (
+                await import(`$i18n/en/settings.json`)
+            ).default,
+        },
+        {
+            locale: 'en',
+            key: 'general',
+            loader: async () => (
+                await import(`$i18n/en/general.json`)
+            ).default,
+        },
+        {
+            locale: 'en',
+            key: 'a11y.general',
+            loader: async () => (
+                await import(`$i18n/en/a11y/general.json`)
+            ).default,
+        },
 
         {
             locale: 'ru',
@@ -82,6 +103,20 @@ export const config: Config = {
                 await import(`$i18n/ru/a11y/meowbalt.json`)
             ).default,
         },
+        {
+            locale: 'ru',
+            key: 'general',
+            loader: async () => (
+                await import(`$i18n/ru/general.json`)
+            ).default,
+        },
+        {
+            locale: 'ru',
+            key: 'a11y.general',
+            loader: async () => (
+                await import(`$i18n/ru/a11y/general.json`)
+            ).default,
+        },
     ],
 };
 
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
index 177017a4..289e7abf 100644
--- a/web/src/routes/+layout.svelte
+++ b/web/src/routes/+layout.svelte
@@ -297,5 +297,6 @@
         color: var(--gray);
         line-height: 1.4;
         padding: 0 var(--padding);
+        white-space: pre-line;
     }
 </style>
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte
index 5d076ad2..4d5a62f7 100644
--- a/web/src/routes/+page.svelte
+++ b/web/src/routes/+page.svelte
@@ -1,10 +1,12 @@
 <script>
+    import { t } from "$lib/i18n/translations";
+
     import Omnibox from "$components/save/Omnibox.svelte";
     import MeowbaltLoaf from "$components/meowbalt/MeowbaltLoaf.svelte";
 </script>
 
 <svelte:head>
-    <title>cobalt</title>
+    <title>{$t("general.cobalt")}</title>
 </svelte:head>
 
 <div id="cobalt-save-container" class="center-column-container">
@@ -13,7 +15,7 @@
         <Omnibox />
     </main>
     <div id="terms-note">
-        by continuing you agree to terms and ethics of use
+        {$t("save.termsNote")}
     </div>
 </div>
 
diff --git a/web/src/routes/about/+page.svelte b/web/src/routes/about/+page.svelte
index 5d70e6dd..1b6783d9 100644
--- a/web/src/routes/about/+page.svelte
+++ b/web/src/routes/about/+page.svelte
@@ -1,10 +1,12 @@
 <script>
+    import { t } from "$lib/i18n/translations";
+
     import Placeholder from "$components/misc/Placeholder.svelte";
 </script>
 
 <svelte:head>
     <title>
-        cobalt: about
+        {$t("general.cobalt")}: {$t("tabs.about")}
     </title>
 </svelte:head>
 
diff --git a/web/src/routes/donate/+page.svelte b/web/src/routes/donate/+page.svelte
index 9959ebe0..51d6af81 100644
--- a/web/src/routes/donate/+page.svelte
+++ b/web/src/routes/donate/+page.svelte
@@ -1,10 +1,12 @@
 <script>
+    import { t } from "$lib/i18n/translations";
+
     import Placeholder from "$components/misc/Placeholder.svelte";
 </script>
 
 <svelte:head>
     <title>
-        cobalt: donate
+        {$t("general.cobalt")}: {$t("tabs.donate")}
     </title>
 </svelte:head>
 
diff --git a/web/src/routes/settings/+layout.svelte b/web/src/routes/settings/+layout.svelte
index 10e61fbd..819938a6 100644
--- a/web/src/routes/settings/+layout.svelte
+++ b/web/src/routes/settings/+layout.svelte
@@ -1,6 +1,8 @@
 <script lang="ts">
     import { page } from "$app/stores";
 
+    import { t } from "$lib/i18n/translations";
+
     import SettingsTab from "$components/settings/SettingsTab.svelte";
     import SettingsSection from "$components/settings/SettingsSection.svelte";
 
@@ -16,7 +18,7 @@
 
     $: currentPageTitle = $page.url.pathname.split("/").at(-1);
     $: stringPageTitle =
-        currentPageTitle !== "settings" ? `/ ${currentPageTitle}` : "";
+        currentPageTitle !== "settings" ? ` / ${$t(`settings.page.${currentPageTitle}`)}` : "";
 
     $: isMobile = screenWidth <= 750;
     $: isHome = $page.url.pathname === `/settings`;
@@ -24,7 +26,7 @@
 
 <svelte:head>
     <title>
-        cobalt: settings {stringPageTitle}
+        {$t("general.cobalt")}: {$t("tabs.settings")}{stringPageTitle}
     </title>
 </svelte:head>
 
@@ -35,19 +37,26 @@
         <div id="settings-header" class:back-visible={!isHome && isMobile}>
             {#if isMobile}
                 {#if !isHome}
-                    <a class="back-button" href="/settings">
+                    <a
+                        class="back-button"
+                        href="/settings"
+                        role="button"
+                        aria-label={$t("a11y.general.back")}
+                    >
                         <IconChevronLeft />
                     </a>
                 {/if}
                 <h3 id="settings-page-title" aria-level="1">
-                    settings
+                    {$t("tabs.settings")}
                     {#if !isHome}
                         <span class="title-slash"> / </span>
-                        {currentPageTitle}
+                        {$t(`settings.page.${currentPageTitle}`)}
                     {/if}
                 </h3>
             {:else}
-                <h2 id="settings-page-title" aria-level="1">settings</h2>
+                <h2 id="settings-page-title" aria-level="1">
+                    {$t("tabs.settings")}
+                </h2>
             {/if}
         </div>
         <nav id="settings-navigation" class:visible-mobile={isMobile && isHome}>
diff --git a/web/src/routes/settings/general/appearance/+page.svelte b/web/src/routes/settings/general/appearance/+page.svelte
index eff1e149..0a268eb9 100644
--- a/web/src/routes/settings/general/appearance/+page.svelte
+++ b/web/src/routes/settings/general/appearance/+page.svelte
@@ -1,44 +1,54 @@
 <script lang="ts">
+    import { t } from "$lib/i18n/translations";
+
+    import { themeOptions } from "$lib/types/settings";
+
     import SettingsCategory from "$components/settings/SettingsCategory.svelte";
     import Switcher from "$components/buttons/Switcher.svelte";
     import SettingsButton from "$components/buttons/SettingsButton.svelte";
     import SettingsToggle from "$components/buttons/SettingsToggle.svelte";
 
-    import { themeOptions } from "$lib/types/settings";
     import LanguageDropdown from "$components/settings/LanguageDropdown.svelte";
 </script>
 
-<SettingsCategory title="theme">
+<SettingsCategory
+    title={$t("settings.theme")}
+    description={$t("settings.theme.description")}
+>
     <Switcher big={true}>
         {#each themeOptions as value}
-            <SettingsButton settingContext="appearance" settingId="theme" settingValue={value}>
-                {value}
+            <SettingsButton
+                settingContext="appearance"
+                settingId="theme"
+                settingValue={value}
+            >
+                {$t(`settings.theme.${value}`)}
             </SettingsButton>
         {/each}
     </Switcher>
 </SettingsCategory>
 
-<SettingsCategory title="accessibility">
+<SettingsCategory title={$t("settings.accessibility")}>
     <SettingsToggle
         settingContext="accessibility"
         settingId="reduceTransparency"
-        title="reduce visual transparency"
-        description="disables blur effects and reduces transparency of surfaces."
+        title={$t("settings.accessibility.transparency.title")}
+        description={$t("settings.accessibility.transparency.description")}
     />
     <SettingsToggle
         settingContext="accessibility"
         settingId="reduceAnimations"
-        title="reduce animations"
-        description="replaces rapid animations with smooth transitions."
+        title={$t("settings.accessibility.animations.title")}
+        description={$t("settings.accessibility.animations.description")}
     />
 </SettingsCategory>
 
-<SettingsCategory title="language">
+<SettingsCategory title={$t("settings.language")}>
     <LanguageDropdown />
     <SettingsToggle
         settingContext="appearance"
         settingId="autoLanguage"
-        title="use default browser language"
-        description="automatically picks the best language for you. if preferred browser language isn't available, english is used instead."
+        title={$t("settings.language.auto.title")}
+        description={$t("settings.language.auto.description")}
     />
 </SettingsCategory>
diff --git a/web/src/routes/settings/save/audio/+page.svelte b/web/src/routes/settings/save/audio/+page.svelte
index f98fd991..0f87ce25 100644
--- a/web/src/routes/settings/save/audio/+page.svelte
+++ b/web/src/routes/settings/save/audio/+page.svelte
@@ -1,42 +1,42 @@
 <script lang="ts">
+    import { t } from "$lib/i18n/translations";
+
+    import { audioFormatOptions } from "$lib/types/settings";
+
     import SettingsCategory from "$components/settings/SettingsCategory.svelte";
     import Switcher from "$components/buttons/Switcher.svelte";
     import SettingsButton from "$components/buttons/SettingsButton.svelte";
     import SettingsToggle from "$components/buttons/SettingsToggle.svelte";
-
-    import { audioFormatOptions } from "$lib/types/settings";
-
-    const audioDescription = `cobalt converts every format but "best", therefore all formats but "best" will be lossy.`;
 </script>
 
 <SettingsCategory
-    title="preferred audio format"
-    description="{audioDescription}"
+    title={$t("settings.audio.format")}
+    description={$t("settings.audio.format.description")}
 >
     <Switcher big={true}>
         {#each audioFormatOptions as value}
             <SettingsButton settingContext="save" settingId="audioFormat" settingValue={value}>
-                {value}
+                {$t(`settings.audio.format.${value}`)}
             </SettingsButton>
         {/each}
     </Switcher>
 
 </SettingsCategory>
 
-<SettingsCategory title="youtube">
+<SettingsCategory title={$t("settings.audio.youtube.dub")}>
     <SettingsToggle
         settingContext="save"
         settingId="youtubeDubBrowserLang"
-        title="use browser language for dubbed videos"
-        description="works even if cobalt ui isn't translated to your language."
+        title={$t("settings.audio.youtube.dub.title")}
+        description={$t("settings.audio.youtube.dub.description")}
     />
 </SettingsCategory>
 
-<SettingsCategory title="tiktok">
+<SettingsCategory title={$t("settings.audio.tiktok.original")}>
     <SettingsToggle
         settingContext="save"
         settingId="tiktokFullAudio"
-        title="use original sound"
-        description="downloads original sound used in the post without any additional changes by the post's author."
+        title={$t("settings.audio.tiktok.original.title")}
+        description={$t("settings.audio.tiktok.original.description")}
     />
 </SettingsCategory>
diff --git a/web/src/routes/settings/save/metadata/+page.svelte b/web/src/routes/settings/save/metadata/+page.svelte
index 9ac990af..860433f1 100644
--- a/web/src/routes/settings/save/metadata/+page.svelte
+++ b/web/src/routes/settings/save/metadata/+page.svelte
@@ -1,40 +1,42 @@
 <script lang="ts">
+    import { t } from "$lib/i18n/translations";
+
+    import { filenameStyleOptions } from "$lib/types/settings";
+
     import SettingsCategory from "$components/settings/SettingsCategory.svelte";
     import Switcher from "$components/buttons/Switcher.svelte";
     import SettingsButton from "$components/buttons/SettingsButton.svelte";
     import SettingsToggle from "$components/buttons/SettingsToggle.svelte";
-
-    import { filenameStyleOptions } from "$lib/types/settings";
 </script>
 
 <SettingsCategory
-    title="filename style"
-    description="very cool description for every style. bla bla bla."
+    title={$t("settings.metadata.filename")}
+    description={$t("settings.metadata.filename.description")}
 >
     <Switcher big={true}>
         {#each filenameStyleOptions as value}
             <SettingsButton settingContext="save" settingId="filenameStyle" settingValue={value}>
-                {value}
+                {$t(`settings.metadata.filename.${value}`)}
             </SettingsButton>
         {/each}
     </Switcher>
 
 </SettingsCategory>
 
-<SettingsCategory title="file metadata">
+<SettingsCategory title={$t("settings.metadata.file")}>
     <SettingsToggle
         settingContext="save"
         settingId="disableMetadata"
-        title="disable file metadata"
-        description="cobalt won't add title, artist, and other info to the file."
+        title={$t("settings.metadata.disable.title")}
+        description={$t("settings.metadata.disable.description")}
     />
 </SettingsCategory>
 
-<SettingsCategory title="saving method">
+<SettingsCategory title={$t("settings.saving.method")}>
     <SettingsToggle
         settingContext="save"
         settingId="downloadPopup"
-        title="ask how to save"
-        description="cobalt will offer you several ways to save the file instead of opening it in a new tab."
+        title={$t("settings.saving.ask.title")}
+        description={$t("settings.saving.ask.description")}
     />
 </SettingsCategory>
diff --git a/web/src/routes/settings/save/video/+page.svelte b/web/src/routes/settings/save/video/+page.svelte
index 8f059255..aa8b9d9b 100644
--- a/web/src/routes/settings/save/video/+page.svelte
+++ b/web/src/routes/settings/save/video/+page.svelte
@@ -1,24 +1,23 @@
 <script lang="ts">
-    import SettingsCategory from "$components/settings/SettingsCategory.svelte";
-    import Switcher from "$components/buttons/Switcher.svelte";
-    import SettingsButton from "$components/buttons/SettingsButton.svelte";
-    import SettingsToggle from "$components/buttons/SettingsToggle.svelte";
+    import { t } from "$lib/i18n/translations";
 
     import { videoQualityOptions } from "$lib/types/settings";
     import { youtubeVideoCodecOptions } from "$lib/types/settings";
 
-    const videoDescription = `if preferred quality isn’t available, closest one is picked instead.`;
-    const codecDescription = `if preferred codec isn’t available, next best is picked instead.`;
+    import SettingsCategory from "$components/settings/SettingsCategory.svelte";
+    import Switcher from "$components/buttons/Switcher.svelte";
+    import SettingsButton from "$components/buttons/SettingsButton.svelte";
+    import SettingsToggle from "$components/buttons/SettingsToggle.svelte";
 </script>
 
 <SettingsCategory
-    title="preferred video quality"
-    description="{videoDescription}"
+    title={$t("settings.video.quality")}
+    description={$t("settings.video.quality.description")}
 >
     <Switcher big={true}>
         {#each videoQualityOptions as value}
             <SettingsButton settingContext="save" settingId="videoQuality" settingValue={value}>
-                {value}
+                {$t(`settings.video.quality.${value}`)}
             </SettingsButton>
         {/each}
     </Switcher>
@@ -26,33 +25,33 @@
 </SettingsCategory>
 
 <SettingsCategory
-    title="preferred youtube codec"
-    description="{codecDescription}"
+    title={$t("settings.video.youtube.codec")}
+    description={$t("settings.video.youtube.codec.description")}
 >
     <Switcher big={true}>
         {#each youtubeVideoCodecOptions as value}
             <SettingsButton settingContext="save" settingId="youtubeVideoCodec" settingValue={value}>
-                {value}
+                {$t(`settings.video.youtube.codec.${value}`)}
             </SettingsButton>
         {/each}
     </Switcher>
 
 </SettingsCategory>
 
-<SettingsCategory title="twitter">
+<SettingsCategory title={$t("settings.video.twitter.gif")}>
     <SettingsToggle
         settingContext="save"
         settingId="twitterGif"
-        title="convert looping videos to GIF"
-        description="GIF conversion is very lossy, end result may be low quality."
+        title={$t("settings.video.twitter.gif.title")}
+        description={$t("settings.video.twitter.gif.description")}
     />
 </SettingsCategory>
 
-<SettingsCategory title="tiktok">
+<SettingsCategory title={$t("settings.video.tiktok.h265")}>
     <SettingsToggle
         settingContext="save"
         settingId="tiktokH265"
-        title="prefer HEVC/H265 format"
-        description="allows 1080p video downloading at cost of compatibility."
+        title={$t("settings.video.tiktok.h265.title")}
+        description={$t("settings.video.tiktok.h265.description")}
     />
 </SettingsCategory>
diff --git a/web/src/routes/updates/+page.svelte b/web/src/routes/updates/+page.svelte
index e3cf1e20..dc9670fa 100644
--- a/web/src/routes/updates/+page.svelte
+++ b/web/src/routes/updates/+page.svelte
@@ -1,10 +1,12 @@
 <script>
+    import { t } from "$lib/i18n/translations";
+
     import Placeholder from "$components/misc/Placeholder.svelte";
 </script>
 
 <svelte:head>
     <title>
-        cobalt: updates
+        {$t("general.cobalt")}: {$t("tabs.donate")}
     </title>
 </svelte:head>