mirror of
https://github.com/wukko/cobalt.git
synced 2025-01-24 19:56:18 +01:00
web: i18n system & navbar translations
dynamic page language and language dropdown!! finally!!
This commit is contained in:
parent
d11874e57f
commit
9939f3b172
19 changed files with 229 additions and 37 deletions
3
web/i18n/en/a11y/tabs.json
Normal file
3
web/i18n/en/a11y/tabs.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"tabPanel": "tabs panel"
|
||||
}
|
7
web/i18n/en/tabs.json
Normal file
7
web/i18n/en/tabs.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"save": "save",
|
||||
"settings": "settings",
|
||||
"updates": "updates",
|
||||
"donate": "donate",
|
||||
"about": "about"
|
||||
}
|
4
web/i18n/languages.json
Normal file
4
web/i18n/languages.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"en": "english",
|
||||
"ru": "русский"
|
||||
}
|
3
web/i18n/ru/a11y/tabs.json
Normal file
3
web/i18n/ru/a11y/tabs.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"tabPanel": "панель вкладок"
|
||||
}
|
7
web/i18n/ru/tabs.json
Normal file
7
web/i18n/ru/tabs.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"save": "скачать",
|
||||
"settings": "настройки",
|
||||
"updates": "новости",
|
||||
"donate": "донаты",
|
||||
"about": "чаво"
|
||||
}
|
32
web/package-lock.json
generated
32
web/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
28
web/src/components/settings/LanguageDropdown.svelte
Normal file
28
web/src/components/settings/LanguageDropdown.svelte
Normal 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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
48
web/src/lib/i18n/translations.ts
Normal file
48
web/src/lib/i18n/translations.ts
Normal 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);
|
|
@ -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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -18,7 +18,8 @@ const config = {
|
|||
strict: true
|
||||
}),
|
||||
alias: {
|
||||
$components: 'src/components'
|
||||
$components: 'src/components',
|
||||
$i18n: 'i18n',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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())
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue