"aLookBackYears": []int{1, 5, 10},
"useBrowserTimezone": true,
"timezone": "UTC",
+ "useBrowserLanguage": true,
+ "language": "en",
}
}
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@popperjs/core": "^2.11.8",
+ "@tolgee/format-icu": "^6.2.7",
+ "@tolgee/svelte": "^6.2.7",
"axios": "^1.7.8",
"bootstrap": "^5.3.3",
"dayjs": "^1.11.13",
"vite": "^5.0.0"
}
},
+ "node_modules/@tolgee/core": {
+ "version": "6.2.7",
+ "resolved": "https://registry.npmjs.org/@tolgee/core/-/core-6.2.7.tgz",
+ "integrity": "sha512-0Au+m9R23/gmeaLJY0X6lKcR2LSy9dW7hEFrpFvPdJoGvxc8XCNZpKlgnm1N534CwmtIBJ4rzOK6vOrSKbl45w==",
+ "license": "MIT"
+ },
+ "node_modules/@tolgee/format-icu": {
+ "version": "6.2.7",
+ "resolved": "https://registry.npmjs.org/@tolgee/format-icu/-/format-icu-6.2.7.tgz",
+ "integrity": "sha512-rLa8EZZVX3pDYC3I6HLnLvTgwLr2PAyOw+7SanS7d7SXp9cvcxbX8O6nqHubv6K7YDqVblcj9FoFbvGBQ22PgQ==",
+ "license": "MIT"
+ },
+ "node_modules/@tolgee/svelte": {
+ "version": "6.2.7",
+ "resolved": "https://registry.npmjs.org/@tolgee/svelte/-/svelte-6.2.7.tgz",
+ "integrity": "sha512-R7J3XO3g5BtUnXwvzljajRAnJeFIPB0qmisEl896L4IJUMHfsddDytaFRP90hlQzEgwRICMtbr43T6XFgSNJrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tolgee/web": "6.2.7"
+ },
+ "peerDependencies": {
+ "svelte": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "node_modules/@tolgee/web": {
+ "version": "6.2.7",
+ "resolved": "https://registry.npmjs.org/@tolgee/web/-/web-6.2.7.tgz",
+ "integrity": "sha512-MAgHGkL5RYREwAjUansJt98fCuQFV4uDfP11NLOCHan2Hx9rpuszv88eXX7enq9CE8eS/Ed2pELiSwb5rlYiKg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@tolgee/core": "6.2.7"
+ }
+ },
"node_modules/@types/bootstrap": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@popperjs/core": "^2.11.8",
+ "@tolgee/format-icu": "^6.2.7",
+ "@tolgee/svelte": "^6.2.7",
"axios": "^1.7.8",
"bootstrap": "^5.3.3",
"dayjs": "^1.11.13",
--- /dev/null
+{
+ "en": "🇺🇸",
+ "de": "🇩🇪",
+ "fr": "🇫🇷"
+}
\ No newline at end of file
import { fly } from 'svelte/transition';
import * as bootstrap from 'bootstrap';
import { offcanvasIsOpen, sameDate } from '$lib/helpers.js';
+ import { getTranslate } from '@tolgee/svelte';
+
+ const { t } = getTranslate();
let { bookmarkDay } = $props();
month: new Date().getMonth() + 1,
year: new Date().getFullYear()
};
- }}>Heute</button
+ }}>{$t('calendar.button_today')}</button
>
</div>
<div class="col-4 d-flex justify-content-end">
import {writable} from 'svelte/store';
+import json from '../i18n/flags.json';
function formatBytes(bytes) {
if (!+bytes) return '0 Bytes';
);
}
-export { formatBytes, sameDate };
+function loadFlagEmoji(language) {
+ return json[language] || '';
+}
+
+export { formatBytes, sameDate, loadFlagEmoji };
export let alwaysShowSidenav = writable(true);
import { API_URL } from '$lib/APIurl.js';
import { tags } from '$lib/tagStore.js';
import TagModal from '$lib/TagModal.svelte';
- import { alwaysShowSidenav } from '$lib/helpers.js';
+ import { alwaysShowSidenav, loadFlagEmoji } from '$lib/helpers.js';
import { templates } from '$lib/templateStore';
import {
faRightFromBracket,
import axios from 'axios';
import { page } from '$app/state';
import { blur, slide, fade } from 'svelte/transition';
+ import { T, getTranslate, getTolgee } from '@tolgee/svelte';
+
+ const { t } = getTranslate();
+ const tolgee = getTolgee(['language']);
let { children } = $props();
let inDuration = 150;
.then((response) => {
$settings = response.data;
aLookBackYears = $settings.aLookBackYears.toString();
+ updateLanguage();
})
.catch((error) => {
console.error(error);
});
});
+ // check if settings have changed (special parsing of aLookBackYears needed)
let settingsHaveChanged = $derived(
JSON.stringify($settings) !== JSON.stringify($tempSettings) ||
JSON.stringify($settings.aLookBackYears) !==
)
);
+ function updateLanguage() {
+ console.log('updateLanguage()');
+ if ($settings.useBrowserLanguage) {
+ let browserLanguage = tolgeesMatchForBrowserLanguage();
+ $tolgee.changeLanguage(
+ browserLanguage === '' ? $tolgee.getInitialOptions().defaultLanguage : browserLanguage
+ );
+ } else {
+ $tolgee.changeLanguage($settings.language);
+ }
+ }
+
+ // Check if Tolgee contains the browser language
+ // returns "" if the browser language is not available
+ // return the language code if it is available
+ function tolgeesMatchForBrowserLanguage() {
+ const browserLanguage = window.navigator.language;
+ const availableLanguages = $tolgee
+ .getInitialOptions()
+ .availableLanguages.map((lang) => lang.toLowerCase());
+
+ // check if tolgee contains an exact match for the browser language OR a match for the first two characters (e.g., 'en' for 'en-US')
+ if (availableLanguages.includes(browserLanguage.toLowerCase())) {
+ return browserLanguage;
+ }
+ if (browserLanguage.length > 2) {
+ const shortBrowserLanguage = browserLanguage.slice(0, 2);
+ if (availableLanguages.includes(shortBrowserLanguage.toLowerCase())) {
+ return shortBrowserLanguage;
+ }
+ }
+
+ return '';
+ }
+
let isSaving = $state(false);
function saveUserSettings() {
if (isSaving) return;
if (response.data.success) {
$settings = $tempSettings;
+ // update language
+ updateLanguage();
+
// show toast
const toast = new bootstrap.Toast(document.getElementById('toastSuccessSaveSettings'));
toast.show();
<!-- -->
<div class="modal-content shadow-lg">
<div class="modal-header">
- <h1>Settings</h1>
+ <h1>{$t('settings.title')}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="modal-body">
</div>
<div id="language">
- <h5>Sprache</h5>
- Bla<br />
- blub <br />
- bla <br />
- blub <br />
+ {#if $tempSettings.useBrowserLanguage !== $settings.useBrowserLanguage || $tempSettings.language !== $settings.language}
+ <div class="unsaved-changes" transition:slide></div>
+ {/if}
+ <h5>🌐 Sprache</h5>
+ <div class="form-check mt-2">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="language"
+ id="language_auto"
+ value={true}
+ bind:group={$tempSettings.useBrowserLanguage}
+ />
+ <label class="form-check-label" for="language_auto">
+ Sprache anhand des Browsers ermitteln (aktuell: <code
+ >{window.navigator.language}</code
+ >
+ {#if tolgeesMatchForBrowserLanguage() !== '' && tolgeesMatchForBrowserLanguage() !== window.navigator.language}
+ ➔ <code>{tolgeesMatchForBrowserLanguage()}</code> wird verwendet
+ {/if}
+ )
+ </label>
+ {#if $tempSettings.useBrowserLanguage && tolgeesMatchForBrowserLanguage() === ''}
+ <div
+ transition:slide
+ disabled={!$settings.useBrowserLanguage}
+ class="alert alert-danger"
+ role="alert"
+ >
+ Die Sprache <code>{window.navigator.language}</code> ist nicht verfügbar. Es
+ wird die Standardsprache
+ <code>{$tolgee.getInitialOptions().defaultLanguage}</code> verwendet.
+ </div>
+ {/if}
+ </div>
+ <div class="form-check mt-2">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="language"
+ id="language_manual"
+ value={false}
+ bind:group={$tempSettings.useBrowserLanguage}
+ />
+ <label class="form-check-label" for="language_manual">
+ Sprache dauerhaft festlegen
+ {#if !$tempSettings.useBrowserLanguage}
+ <select
+ transition:slide
+ class="form-select"
+ bind:value={$tempSettings.language}
+ disabled={$tempSettings.useBrowserLanguage}
+ >
+ {#each $tolgee.getInitialOptions().availableLanguages as lang}
+ <option value={lang}>{loadFlagEmoji(lang)} {lang}</option>
+ {/each}
+ </select>
+ {/if}
+ </label>
+ </div>
</div>
<div id="timezone">
{#if $tempSettings.useBrowserTimezone !== $settings.useBrowserTimezone || ($tempSettings.timezone !== undefined && $tempSettings.timezone?.value !== $settings.timezone?.value)}
import { templates, insertTemplate } from '$lib/templateStore';
import ALookBack from '$lib/ALookBack.svelte';
import { marked } from 'marked';
+ import { T, getTranslate } from '@tolgee/svelte';
+
+ const { t } = getTranslate();
axios.interceptors.request.use((config) => {
config.withCredentials = true;
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
- <h5 class="modal-title">Tag vollständig löschen?</h5>
+ <h5 class="modal-title">{$t('modal.deleteDay.title')}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
></button>
</div>
<div class="modal-body">
- Du löschst <b><u>sämtliche Daten</u></b> vom
- <b><u>{`${$selectedDate.day}.${$selectedDate.month}.${$selectedDate.year}`}</u></b>!<br
- /><br />
- Dies beinhaltet:
+ {@html $t('modal.deleteDay.description', {
+ day: $selectedDate.day,
+ month: $selectedDate.month,
+ year: $selectedDate.year
+ })}
+ <br /><br />
+ {$t('modal.deleteDay.thisIncludes')}
<ul>
{#snippet deleteDayBool(available, description)}
<li class={available ? 'text-decoration-underline' : 'text-muted fst-italic'}>
{#snippet deleteDayCount(item, description)}
<li class={item.length > 0 ? 'text-decoration-underline' : 'text-muted fst-italic'}>
- {item.length}
{description}
</li>
{/snippet}
- {@render deleteDayBool(logDateWritten !== '', 'Tagebucheintrag')}
- {@render deleteDayBool(historyAvailable, 'Verlauf')}
-
- {@render deleteDayCount(filesOfDay, 'Dateien')}
- {@render deleteDayCount(selectedTags, 'Tags')}
- {@render deleteDayBool($cal.daysBookmarked.includes($selectedDate.day), 'Lesezeichen')}
+ {@render deleteDayBool(logDateWritten !== '', $t('modal.deleteDay.logEntry'))}
+ {@render deleteDayBool(historyAvailable, $t('modal.deleteDay.history'))}
+
+ {@render deleteDayCount(
+ filesOfDay,
+ $t('modal.deleteDay.files', { files: filesOfDay.length })
+ )}
+ {@render deleteDayCount(
+ selectedTags,
+ $t('modal.deleteDay.tags', { tags: selectedTags.length })
+ )}
+ {@render deleteDayBool(
+ $cal.daysBookmarked.includes($selectedDate.day),
+ $t('modal.deleteDay.bookmark')
+ )}
</ul>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
+ >{$t('modal.deleteDay.button_close')}</button
+ >
<button
onclick={() => deleteDay()}
type="button"
- class="btn btn-primary"
- data-bs-dismiss="modal">Löschen</button
+ class="btn btn-danger"
+ data-bs-dismiss="modal">{$t('modal.deleteDay.button_delete')}</button
>
</div>
</div>
import trianglify from 'trianglify';
import { alwaysShowSidenav } from '$lib/helpers.js';
import * as bootstrap from 'bootstrap';
+ import {
+ TolgeeProvider,
+ Tolgee,
+ DevTools,
+ LanguageDetector,
+ LanguageStorage
+ } from '@tolgee/svelte';
+ import { FormatIcu } from '@tolgee/format-icu';
+ import { use } from 'marked';
+
+ const tolgee = Tolgee()
+ .use(DevTools())
+ .use(FormatIcu())
+ .use(LanguageStorage())
+ .init({
+ availableLanguages: ['en', 'de', 'fr'],
+ defaultLanguage: 'en',
+
+ // for development
+ apiUrl: import.meta.env.VITE_TOLGEE_API_URL,
+ apiKey: import.meta.env.VITE_TOLGEE_API_KEY
+ });
let { children } = $props();
let inDuration = 150;
let routeToFromLoginKey = $derived(page.url.pathname === '/login');
</script>
-<main class="d-flex flex-column">
- <div class="wrapper h-100">
- {#key routeToFromLoginKey}
+<TolgeeProvider {tolgee}>
+ <main class="d-flex flex-column">
+ <div class="wrapper h-100">
+ {#key routeToFromLoginKey}
+ <div
+ class="transition-wrapper h-100"
+ out:blur={{ duration: outDuration }}
+ in:blur={{ duration: inDuration, delay: outDuration }}
+ >
+ {@render children()}
+ </div>
+ {/key}
+ </div>
+
+ <div class="toast-container position-fixed bottom-0 end-0 p-3">
<div
- class="transition-wrapper h-100"
- out:blur={{ duration: outDuration }}
- in:blur={{ duration: inDuration, delay: outDuration }}
+ id="toastAvailableBackupCodesWarning"
+ class="toast align-items-center {available_backup_codes > 3
+ ? 'text-bg-warning'
+ : 'text-bg-danger'}"
+ role="alert"
+ aria-live="assertive"
+ aria-atomic="true"
>
- {@render children()}
- </div>
- {/key}
- </div>
-
- <div class="toast-container position-fixed bottom-0 end-0 p-3">
- <div
- id="toastAvailableBackupCodesWarning"
- class="toast align-items-center {available_backup_codes > 3
- ? 'text-bg-warning'
- : 'text-bg-danger'}"
- role="alert"
- aria-live="assertive"
- aria-atomic="true"
- >
- <div class="d-flex">
- <div class="toast-body">Noch {available_backup_codes} Backup-Codes verfügbar!</div>
+ <div class="d-flex">
+ <div class="toast-body">Noch {available_backup_codes} Backup-Codes verfügbar!</div>
+ </div>
</div>
</div>
- </div>
-</main>
+ </main>
+</TolgeeProvider>
<style>
main {
import axios from 'axios';
import { goto } from '$app/navigation';
import { API_URL } from '$lib/APIurl.js';
+ import { getTranslate, getTolgee } from '@tolgee/svelte';
+ import { loadFlagEmoji } from '$lib/helpers.js';
+
+ const { t } = getTranslate();
+ const tolgee = getTolgee(['language']);
+ let selectedLanguage = $state('');
let show_login_failed = $state(false);
let show_login_warning_empty_fields = $state(false);
migration_phases.indexOf(migration_phase)
);
+ // Check if Tolgee contains the browser language
+ // returns "" if the browser language is not available
+ // return the language code if it is available
+ function tolgeesMatchForBrowserLanguage() {
+ const browserLanguage = window.navigator.language;
+ const availableLanguages = $tolgee
+ .getInitialOptions()
+ .availableLanguages.map((lang) => lang.toLowerCase());
+
+ // check if tolgee contains an exact match for the browser language OR a match for the first two characters (e.g., 'en' for 'en-US')
+ if (availableLanguages.includes(browserLanguage.toLowerCase())) {
+ return browserLanguage;
+ }
+ if (browserLanguage.length > 2) {
+ const shortBrowserLanguage = browserLanguage.slice(0, 2);
+ if (availableLanguages.includes(shortBrowserLanguage.toLowerCase())) {
+ return shortBrowserLanguage;
+ }
+ }
+
+ return '';
+ }
+
onMount(() => {
+ selectedLanguage = tolgeesMatchForBrowserLanguage();
+ if (selectedLanguage === '') {
+ selectedLanguage = $tolgee.getInitialOptions().defaultLanguage;
+ }
+ $tolgee.changeLanguage(selectedLanguage);
+
// if params error=440 or error=401, show toast
if (window.location.search.includes('error=440')) {
const toast = new bootstrap.Toast(document.getElementById('toastLoginExpired'));
placeholder="Username"
autofocus
/>
- <label for="loginUsername">Username</label>
+ <label for="loginUsername">{$t('login.username')}</label>
</div>
<div class="form-floating mb-3">
<input
id="loginPassword"
placeholder="Password"
/>
- <label for="loginPassword">Password</label>
+ <label for="loginPassword">{$t('login.password')}</label>
</div>
{#if is_migrating || migration_phase == 'completed'}
<div class="alert alert-info" role="alert">
<span class="visually-hidden">Loading...</span>
</div>
{/if}
- Login
+ {$t('login.login')}
</button>
</div>
</form>
aria-expanded="false"
aria-controls="collapseTwo"
>
- Registrierung
+ {$t('login.create_account')}
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#loginAccordion">
id="registerUsername"
placeholder="Username"
/>
- <label for="registerUsername">Username</label>
+ <label for="registerUsername">{$t('login.username')}</label>
</div>
<div class="form-floating mb-3">
<input
id="registerPassword"
placeholder="Password"
/>
- <label for="registerPassword">Password</label>
+ <label for="registerPassword">{$t('login.password')}</label>
</div>
<div class="form-floating mb-3">
<input
id="registerPassword2"
placeholder="Password bestätigen"
/>
- <label for="registerPassword2">Password bestätigen</label>
+ <label for="registerPassword2">{$t('login.confirm_password')}</label>
</div>
{#if !registration_allowed}
<div class="alert alert-danger" role="alert">
</div>
</div>
+ <div class="language-select-wrapper">
+ <div class="input-group mb-3">
+ <span class="input-group-text" id="basic-addon1">🌐</span>
+ <select
+ class="form-select"
+ bind:value={selectedLanguage}
+ onchange={() => {
+ $tolgee.changeLanguage(selectedLanguage);
+ }}
+ >
+ {#each $tolgee.getInitialOptions().availableLanguages as lang}
+ <option value={lang}>{loadFlagEmoji(lang)} {lang}</option>
+ {/each}
+ </select>
+ </div>
+ </div>
+
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div
id="toastLoginExpired"
</div>
<style>
+ .language-select-wrapper {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ }
+
.progress-item {
opacity: 0.5;
}