mirror of
https://github.com/imputnet/cobalt.git
synced 2025-01-27 08:52:10 +00:00
web: i18n system & navbar translations
dynamic page language and language dropdown!! finally!!
This commit is contained in:
parent
d11874e57f
commit
9939f3b172
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-variable/noto-sans-mono": "^5.0.20",
|
||||||
"@fontsource/ibm-plex-mono": "^5.0.13",
|
"@fontsource/ibm-plex-mono": "^5.0.13",
|
||||||
"@tabler/icons-svelte": "^3.6.0",
|
"@tabler/icons-svelte": "^3.6.0",
|
||||||
|
"sveltekit-i18n": "^2.4.2",
|
||||||
"ts-deepmerge": "^7.0.0"
|
"ts-deepmerge": "^7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -894,6 +895,21 @@
|
||||||
"vite": "^5.0.0"
|
"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": {
|
"node_modules/@tabler/icons": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.6.0.tgz",
|
"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": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"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-variable/noto-sans-mono": "^5.0.20",
|
||||||
"@fontsource/ibm-plex-mono": "^5.0.13",
|
"@fontsource/ibm-plex-mono": "^5.0.13",
|
||||||
"@tabler/icons-svelte": "^3.6.0",
|
"@tabler/icons-svelte": "^3.6.0",
|
||||||
|
"sveltekit-i18n": "^2.4.2",
|
||||||
"ts-deepmerge": "^7.0.0"
|
"ts-deepmerge": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1, maximum-scale=1" />
|
<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">
|
<script lang="ts">
|
||||||
|
import { t } from "$lib/i18n/translations";
|
||||||
|
|
||||||
import CobaltLogo from "$components/sidebar/CobaltLogo.svelte";
|
import CobaltLogo from "$components/sidebar/CobaltLogo.svelte";
|
||||||
import SidebarTab from "$components/sidebar/SidebarTab.svelte";
|
import SidebarTab from "$components/sidebar/SidebarTab.svelte";
|
||||||
|
|
||||||
|
@ -23,7 +25,7 @@
|
||||||
|
|
||||||
<svelte:window bind:innerWidth={screenWidth} />
|
<svelte:window bind:innerWidth={screenWidth} />
|
||||||
|
|
||||||
<nav id="sidebar">
|
<nav id="sidebar" aria-label={$t("a11y.tabs.tabPanel")}>
|
||||||
<CobaltLogo />
|
<CobaltLogo />
|
||||||
<div id="sidebar-tabs">
|
<div id="sidebar-tabs">
|
||||||
<div id="sidebar-actions" class="sidebar-inner-container">
|
<div id="sidebar-actions" class="sidebar-inner-container">
|
||||||
|
|
|
@ -1,33 +1,31 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
|
|
||||||
|
import { t } from "$lib/i18n/translations";
|
||||||
|
|
||||||
export let tabName: string;
|
export let tabName: string;
|
||||||
export let tabLink: string;
|
export let tabLink: string;
|
||||||
|
|
||||||
const firstTabPage = [
|
const firstTabPage = ["save", "settings", "updates"];
|
||||||
"save",
|
|
||||||
"settings",
|
|
||||||
"updates"
|
|
||||||
];
|
|
||||||
|
|
||||||
let tab: HTMLElement;
|
let tab: HTMLElement;
|
||||||
|
|
||||||
$: currentTab = $page.url.pathname.split('/')[1];
|
$: currentTab = $page.url.pathname.split("/")[1];
|
||||||
$: baseTabPath = tabLink.split('/')[1]
|
$: baseTabPath = tabLink.split("/")[1];
|
||||||
|
|
||||||
$: isTabActive = currentTab === baseTabPath;
|
$: isTabActive = currentTab === baseTabPath;
|
||||||
|
|
||||||
const showTab = (e: HTMLElement | undefined) => {
|
const showTab = (e: HTMLElement | undefined) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
e.scrollIntoView({
|
e.scrollIntoView({
|
||||||
inline: firstTabPage.includes(tabName) ? 'end' : 'start',
|
inline: firstTabPage.includes(tabName) ? "end" : "start",
|
||||||
behavior: 'smooth'
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
$: if (isTabActive) {
|
$: if (isTabActive) {
|
||||||
showTab(tab)
|
showTab(tab);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -38,9 +36,10 @@
|
||||||
href={tabLink}
|
href={tabLink}
|
||||||
bind:this={tab}
|
bind:this={tab}
|
||||||
on:focus={() => showTab(tab)}
|
on:focus={() => showTab(tab)}
|
||||||
|
role="tab"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
{tabName}
|
{$t(`tabs.${tabName}`)}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<style>
|
<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 isAndroid = ua.includes("android") || ua.includes("diordna");
|
||||||
const isMobile = isIOS || isAndroid;
|
const isMobile = isIOS || isAndroid;
|
||||||
|
|
||||||
const deviceInfo = {
|
const preferredLocale = navigator.language.toLowerCase().slice(0, 2);
|
||||||
|
|
||||||
|
const device = {
|
||||||
isIOS,
|
isIOS,
|
||||||
isAndroid,
|
isAndroid,
|
||||||
isMobile,
|
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";
|
import type { CobaltSettings } from "$lib/types/settings";
|
||||||
|
|
||||||
const defaultSettings: CobaltSettings = {
|
const defaultSettings: CobaltSettings = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
accessibility: {
|
accessibility: {
|
||||||
reduceAnimations: false,
|
reduceAnimations: false,
|
||||||
reduceTransparency: false
|
reduceTransparency: false,
|
||||||
},
|
},
|
||||||
appearance: {
|
appearance: {
|
||||||
theme: "auto"
|
theme: "auto",
|
||||||
|
language: defaultLocale,
|
||||||
|
autoLanguage: true,
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
customProcessingEndpoint: "",
|
customProcessingEndpoint: "",
|
||||||
seenOnboarding: false,
|
seenOnboarding: false,
|
||||||
seenSafetyWarning: false
|
seenSafetyWarning: false,
|
||||||
},
|
},
|
||||||
save: {
|
save: {
|
||||||
audioFormat: "mp3",
|
audioFormat: "mp3",
|
||||||
|
@ -25,22 +28,11 @@ const defaultSettings: CobaltSettings = {
|
||||||
twitterGif: false,
|
twitterGif: false,
|
||||||
videoQuality: "720",
|
videoQuality: "720",
|
||||||
youtubeVideoCodec: "h264",
|
youtubeVideoCodec: "h264",
|
||||||
youtubeDubBrowserLang: false
|
youtubeDubBrowserLang: false,
|
||||||
},
|
},
|
||||||
privacy: {
|
privacy: {
|
||||||
trafficAnalytics: true
|
trafficAnalytics: true,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
export default defaultSettings;
|
|
||||||
|
|
||||||
export const settingArrays = {
|
export default defaultSettings;
|
||||||
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"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import languages from '$i18n/languages.json';
|
||||||
|
|
||||||
export type CobaltSettingsAccessibility = {
|
export type CobaltSettingsAccessibility = {
|
||||||
reduceAnimations: boolean,
|
reduceAnimations: boolean,
|
||||||
reduceTransparency: boolean,
|
reduceTransparency: boolean,
|
||||||
|
@ -12,6 +14,8 @@ export const youtubeVideoCodecOptions = ["h264", "av1", "vp9"] as const;
|
||||||
|
|
||||||
type CobaltSettingsAppearance = {
|
type CobaltSettingsAppearance = {
|
||||||
theme: typeof themeOptions[number],
|
theme: typeof themeOptions[number],
|
||||||
|
language: keyof typeof languages,
|
||||||
|
autoLanguage: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
type CobaltSettingsGeneral = {
|
type CobaltSettingsGeneral = {
|
||||||
|
|
|
@ -1,2 +1,39 @@
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
export const ssr = false;
|
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 SettingsToggle from "$components/buttons/SettingsToggle.svelte";
|
||||||
|
|
||||||
import { themeOptions } from "$lib/types/settings";
|
import { themeOptions } from "$lib/types/settings";
|
||||||
|
import LanguageDropdown from "$components/settings/LanguageDropdown.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCategory title="theme">
|
<SettingsCategory title="theme">
|
||||||
|
@ -31,3 +32,13 @@
|
||||||
description="replaces rapid animations with smooth transitions."
|
description="replaces rapid animations with smooth transitions."
|
||||||
/>
|
/>
|
||||||
</SettingsCategory>
|
</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
|
strict: true
|
||||||
}),
|
}),
|
||||||
alias: {
|
alias: {
|
||||||
$components: 'src/components'
|
$components: 'src/components',
|
||||||
|
$i18n: 'i18n',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig, searchForWorkspaceRoot } from "vite";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()],
|
plugins: [
|
||||||
|
sveltekit()
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
fs: {
|
||||||
|
allow: [
|
||||||
|
searchForWorkspaceRoot(process.cwd())
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue