web: i18n system & navbar translations

dynamic page language and language dropdown!! finally!!
This commit is contained in:
wukko 2024-07-03 00:16:03 +06:00
parent d11874e57f
commit 9939f3b172
No known key found for this signature in database
GPG key ID: 3E30B3F26C7B4AA2
19 changed files with 229 additions and 37 deletions

View file

@ -0,0 +1,3 @@
{
"tabPanel": "tabs panel"
}

7
web/i18n/en/tabs.json Normal file
View file

@ -0,0 +1,7 @@
{
"save": "save",
"settings": "settings",
"updates": "updates",
"donate": "donate",
"about": "about"
}

4
web/i18n/languages.json Normal file
View file

@ -0,0 +1,4 @@
{
"en": "english",
"ru": "русский"
}

View file

@ -0,0 +1,3 @@
{
"tabPanel": "панель вкладок"
}

7
web/i18n/ru/tabs.json Normal file
View file

@ -0,0 +1,7 @@
{
"save": "скачать",
"settings": "настройки",
"updates": "новости",
"donate": "донаты",
"about": "чаво"
}

32
web/package-lock.json generated
View file

@ -12,6 +12,7 @@
"@fontsource-variable/noto-sans-mono": "^5.0.20",
"@fontsource/ibm-plex-mono": "^5.0.13",
"@tabler/icons-svelte": "^3.6.0",
"sveltekit-i18n": "^2.4.2",
"ts-deepmerge": "^7.0.0"
},
"devDependencies": {
@ -894,6 +895,21 @@
"vite": "^5.0.0"
}
},
"node_modules/@sveltekit-i18n/base": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/@sveltekit-i18n/base/-/base-1.3.7.tgz",
"integrity": "sha512-kg1kql1/ro/lIudwFiWrv949Q07gmweln87tflUZR51MNdXXzK4fiJQv5Mw50K/CdQ5BOk/dJ0WOH2vOtBI6yw==",
"license": "MIT",
"peerDependencies": {
"svelte": ">=3.49.0"
}
},
"node_modules/@sveltekit-i18n/parser-default": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@sveltekit-i18n/parser-default/-/parser-default-1.1.1.tgz",
"integrity": "sha512-/gtzLlqm/sox7EoPKD56BxGZktK/syGc79EbJAPWY5KVitQD9SM0TP8yJCqDxTVPk7Lk0WJhrBGUE2Nn0f5M1w==",
"license": "MIT"
},
"node_modules/@tabler/icons": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.6.0.tgz",
@ -3002,6 +3018,22 @@
}
}
},
"node_modules/sveltekit-i18n": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/sveltekit-i18n/-/sveltekit-i18n-2.4.2.tgz",
"integrity": "sha512-hjRWn4V4DBL8JQKJoJa3MRvn6d32Zo+rWkoSP5bsQ/XIAguPdQUZJ8LMe6Nc1rST8WEVdu9+vZI3aFdKYGR3+Q==",
"license": "MIT",
"workspaces": [
"./examples/*/"
],
"dependencies": {
"@sveltekit-i18n/base": "~1.3.0",
"@sveltekit-i18n/parser-default": "~1.1.0"
},
"peerDependencies": {
"svelte": ">=3.49.0"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",

View file

@ -41,6 +41,7 @@
"@fontsource-variable/noto-sans-mono": "^5.0.20",
"@fontsource/ibm-plex-mono": "^5.0.13",
"@tabler/icons-svelte": "^3.6.0",
"sveltekit-i18n": "^2.4.2",
"ts-deepmerge": "^7.0.0"
}
}

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1, maximum-scale=1" />

View file

@ -0,0 +1,28 @@
<script lang="ts">
import settings, { updateSetting } from "$lib/settings";
import { t, locale, locales } from "$lib/i18n/translations";
import languages from "$i18n/languages.json";
$: currentSetting = $settings.appearance.language;
const updateLocale = (lang: string) => {
updateSetting({
appearance: {
language: lang as keyof typeof languages,
},
})
}
</script>
<select
id="setting-dropdown-appearance-language"
bind:value={$locale}
on:change={() => updateLocale($locale)}
>
{#each $locales as value}
<option value={value} selected={currentSetting === value}>
{$t(`languages.${value}`)}
</option>
{/each}
</select>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import { t } from "$lib/i18n/translations";
import CobaltLogo from "$components/sidebar/CobaltLogo.svelte";
import SidebarTab from "$components/sidebar/SidebarTab.svelte";
@ -23,7 +25,7 @@
<svelte:window bind:innerWidth={screenWidth} />
<nav id="sidebar">
<nav id="sidebar" aria-label={$t("a11y.tabs.tabPanel")}>
<CobaltLogo />
<div id="sidebar-tabs">
<div id="sidebar-actions" class="sidebar-inner-container">

View file

@ -1,33 +1,31 @@
<script lang="ts">
import { page } from "$app/stores";
import { t } from "$lib/i18n/translations";
export let tabName: string;
export let tabLink: string;
const firstTabPage = [
"save",
"settings",
"updates"
];
const firstTabPage = ["save", "settings", "updates"];
let tab: HTMLElement;
$: currentTab = $page.url.pathname.split('/')[1];
$: baseTabPath = tabLink.split('/')[1]
$: currentTab = $page.url.pathname.split("/")[1];
$: baseTabPath = tabLink.split("/")[1];
$: isTabActive = currentTab === baseTabPath;
const showTab = (e: HTMLElement | undefined) => {
if (e) {
e.scrollIntoView({
inline: firstTabPage.includes(tabName) ? 'end' : 'start',
behavior: 'smooth'
inline: firstTabPage.includes(tabName) ? "end" : "start",
behavior: "smooth",
});
}
}
};
$: if (isTabActive) {
showTab(tab)
showTab(tab);
}
</script>
@ -38,9 +36,10 @@
href={tabLink}
bind:this={tab}
on:focus={() => showTab(tab)}
role="tab"
>
<slot></slot>
{tabName}
{$t(`tabs.${tabName}`)}
</a>
<style>

View file

@ -4,10 +4,14 @@ const isIOS = ua.includes("iphone os") || (ua.includes("mac os") && navigator.ma
const isAndroid = ua.includes("android") || ua.includes("diordna");
const isMobile = isIOS || isAndroid;
const deviceInfo = {
const preferredLocale = navigator.language.toLowerCase().slice(0, 2);
const device = {
isIOS,
isAndroid,
isMobile,
preferredLocale,
}
export default deviceInfo;
export default device;

View file

@ -0,0 +1,48 @@
import i18n from 'sveltekit-i18n';
import type { Config } from 'sveltekit-i18n';
import languages from '$i18n/languages.json';
export const defaultLocale = 'en';
export const config: Config = {
translations: {
en: { languages },
ru: { languages },
},
loaders: [
{
locale: 'en',
key: 'tabs',
loader: async () => (
await import(`$i18n/en/tabs.json`)
).default,
},
{
locale: 'en',
key: 'a11y.tabs',
loader: async () => (
await import(`$i18n/en/a11y/tabs.json`)
).default,
},
{
locale: 'ru',
key: 'tabs',
loader: async () => (
await import(`$i18n/ru/tabs.json`)
).default,
},
{
locale: 'ru',
key: 'a11y.tabs',
loader: async () => (
await import(`$i18n/ru/a11y/tabs.json`)
).default,
},
],
};
export const {
t, loading, locales, locale, translations,
loadTranslations, addTranslations, setLocale, setRoute
} = new i18n(config);

View file

@ -1,18 +1,21 @@
import { defaultLocale } from "$lib/i18n/translations";
import type { CobaltSettings } from "$lib/types/settings";
const defaultSettings: CobaltSettings = {
schemaVersion: 1,
accessibility: {
reduceAnimations: false,
reduceTransparency: false
reduceTransparency: false,
},
appearance: {
theme: "auto"
theme: "auto",
language: defaultLocale,
autoLanguage: true,
},
general: {
customProcessingEndpoint: "",
seenOnboarding: false,
seenSafetyWarning: false
seenSafetyWarning: false,
},
save: {
audioFormat: "mp3",
@ -25,22 +28,11 @@ const defaultSettings: CobaltSettings = {
twitterGif: false,
videoQuality: "720",
youtubeVideoCodec: "h264",
youtubeDubBrowserLang: false
youtubeDubBrowserLang: false,
},
privacy: {
trafficAnalytics: true
}
trafficAnalytics: true,
},
}
export default defaultSettings;
export const settingArrays = {
appearance: {
theme: ["auto", "light", "dark"]
},
save: {
audioFormat: ["best", "mp3", "ogg", "wav", "opus"],
filenameStyle: ["classic", "basic", "pretty", "nerdy"],
videoQuality: ["max", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
youtubeVideoCodec: ["h264", "av1", "vp9"],
},
}
export default defaultSettings;

View file

@ -1,3 +1,5 @@
import languages from '$i18n/languages.json';
export type CobaltSettingsAccessibility = {
reduceAnimations: boolean,
reduceTransparency: boolean,
@ -12,6 +14,8 @@ export const youtubeVideoCodecOptions = ["h264", "av1", "vp9"] as const;
type CobaltSettingsAppearance = {
theme: typeof themeOptions[number],
language: keyof typeof languages,
autoLanguage: boolean,
};
type CobaltSettingsGeneral = {

View file

@ -1,2 +1,39 @@
export const prerender = true;
export const ssr = false;
import { browser } from '$app/environment';
import { get } from 'svelte/store';
import type { Load } from '@sveltejs/kit';
import languages from '$i18n/languages.json';
import { loadTranslations, defaultLocale } from '$lib/i18n/translations';
import device from '$lib/device.js';
export const load: Load = async ({ url }) => {
const { pathname } = url;
let preferredLocale = defaultLocale;
if (browser) {
const settings = get((await import('$lib/settings')).default);
const deviceLanguage = device.preferredLocale;
const settingsLanguage = settings.appearance.language;
const isValid = (lang: string) => (
Object.keys(languages).includes(lang)
);
if (settings.appearance.autoLanguage) {
if (isValid(deviceLanguage)) {
preferredLocale = deviceLanguage;
}
} else if (isValid(settingsLanguage)) {
preferredLocale = settingsLanguage
}
}
await loadTranslations(preferredLocale, pathname);
return {};
}

View file

@ -5,6 +5,7 @@
import SettingsToggle from "$components/buttons/SettingsToggle.svelte";
import { themeOptions } from "$lib/types/settings";
import LanguageDropdown from "$components/settings/LanguageDropdown.svelte";
</script>
<SettingsCategory title="theme">
@ -31,3 +32,13 @@
description="replaces rapid animations with smooth transitions."
/>
</SettingsCategory>
<SettingsCategory title="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."
/>
</SettingsCategory>

View file

@ -18,7 +18,8 @@ const config = {
strict: true
}),
alias: {
$components: 'src/components'
$components: 'src/components',
$i18n: 'i18n',
}
}
};

View file

@ -1,6 +1,15 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
import { defineConfig, searchForWorkspaceRoot } from "vite";
export default defineConfig({
plugins: [sveltekit()],
plugins: [
sveltekit()
],
server: {
fs: {
allow: [
searchForWorkspaceRoot(process.cwd())
]
}
}
});