integrated i18n-support with tolgee
authorPhiTux <redacted>
Sun, 17 Aug 2025 22:05:05 +0000 (00:05 +0200)
committerPhiTux <redacted>
Sun, 17 Aug 2025 22:05:05 +0000 (00:05 +0200)
backend/handlers/users.go
frontend/package-lock.json
frontend/package.json
frontend/src/i18n/flags.json [new file with mode: 0644]
frontend/src/lib/Datepicker.svelte
frontend/src/lib/helpers.js
frontend/src/routes/(authed)/+layout.svelte
frontend/src/routes/(authed)/write/+page.svelte
frontend/src/routes/+layout.svelte
frontend/src/routes/login/+page.svelte

index 7582c02d3c252640539e31a935274f7d042128f3..cc9845724681f3caad4fefe084c4257d742a045b 100644 (file)
@@ -426,6 +426,8 @@ func GetDefaultSettings() map[string]any {
                "aLookBackYears":             []int{1, 5, 10},
                "useBrowserTimezone":         true,
                "timezone":                   "UTC",
+               "useBrowserLanguage":         true,
+               "language":                   "en",
        }
 }
 
index e8ceccff2286a7d351c73b3c98eea19804c8c33b..ba164823e79fc4425313e8a420a88e5b9e42115a 100644 (file)
@@ -10,6 +10,8 @@
                        "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",
index 61ef0b5f9e9cfff45e87a8f4b5f4d6ae4518e154..582f257559df479fe32e41b8a287128717453759 100644 (file)
@@ -34,6 +34,8 @@
        "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",
diff --git a/frontend/src/i18n/flags.json b/frontend/src/i18n/flags.json
new file mode 100644 (file)
index 0000000..3abd1b4
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "en": "🇺🇸",
+  "de": "🇩🇪",
+  "fr": "🇫🇷"
+}
\ No newline at end of file
index 2d510d06e8643c959d7959c519038441a5dc22b3..d205d5ad160928beb955ec46f9f795a9105165f3 100644 (file)
@@ -4,6 +4,9 @@
        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">
index 086fa6d68b318fbb3768fcbcc40a6890cf193683..a453295e8f5235a720fdf916728e04a3fec48e3f 100644 (file)
@@ -1,4 +1,5 @@
 import {writable} from 'svelte/store';
+import json from '../i18n/flags.json';
 
 function formatBytes(bytes) {
        if (!+bytes) return '0 Bytes';
@@ -20,7 +21,11 @@ function sameDate(date1, date2) {
        );
 }
 
-export { formatBytes, sameDate };
+function loadFlagEmoji(language) {
+       return json[language] || '';
+}
+
+export { formatBytes, sameDate, loadFlagEmoji };
 
 export let alwaysShowSidenav = writable(true);
 
index e4a35f34f249a50d39f9c711875776ecf950ee9a..5896330c0bc7e0093223b02a1ea05373b632dffd 100644 (file)
@@ -14,7 +14,7 @@
        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)}
index 4b87a275b1c686dc1cb18a16c5436805f12b7d97..4618e59e58ab89e2eb197917447b984b1fe03fc5 100644 (file)
@@ -35,6 +35,9 @@
        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>
index 827e109533b55571af8d1abd42df4bc92f2b4f62..f0d2e024887f18db95bbde063704b4b3870f4504 100644 (file)
        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 {
index 670bfc6de32d4382da33089e974cab265f5f2e77..c8285dfdd23d43aa57921d5506d4c38c4fe353a5 100644 (file)
@@ -5,6 +5,12 @@
        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;
        }
git clone https://git.99rst.org/PROJECT