diff options
Diffstat (limited to 'apps/projects/src/app')
23 files changed, 1502 insertions, 257 deletions
diff --git a/apps/projects/src/app/index.scss b/apps/projects/src/app/index.scss index 4794787..0892d63 100644 --- a/apps/projects/src/app/index.scss +++ b/apps/projects/src/app/index.scss @@ -36,3 +36,4 @@ @use '../../web-shared/src/styles/components/custom-checkbox'; @use '../../web-shared/src/styles/components/menu'; @use '../../web-shared/src/styles/components/user-menu'; +@use '../../web-shared/src/styles/components/light-dark-switch'; diff --git a/apps/projects/src/app/index.svelte b/apps/projects/src/app/index.svelte index 77d290d..e397de3 100644 --- a/apps/projects/src/app/index.svelte +++ b/apps/projects/src/app/index.svelte @@ -1,53 +1,86 @@ <svelte:options immutable={true}/> <svelte:window bind:online={online}/> -<script> - import {logout_user} from "$app/lib/services/user-service"; - import Router from "svelte-spa-router"; - import {wrap} from "svelte-spa-router/wrap"; - import {QueryClient, QueryClientProvider} from "@sveltestack/svelte-query"; - import {is_active} from "$shared/lib/session"; - import UiWorkbench from "$app/pages/ui-workbench.svelte"; - import NotFound from "$app/pages/not-found.svelte"; - import Home from "$app/pages/home.svelte"; - import Settings from "$app/pages/settings.svelte"; - import Data from "$app/pages/data.svelte"; - import PreHeader from "$shared/components/pre-header.svelte"; +<script lang="ts"> + import {logout_user} from "$app/lib/services/user-service"; + import {currentLocale, preffered_or_default} from "$app/lib/stores/locale"; + import {CookieNames} from "$shared/lib/configuration"; + import {get_cookie} from "$shared/lib/helpers"; + import {Temporal} from "@js-temporal/polyfill"; + import {onMount} from "svelte"; + import Router from "svelte-spa-router"; + import {wrap} from "svelte-spa-router/wrap"; + import {QueryClient, QueryClientProvider} from "@sveltestack/svelte-query"; + import {is_active} from "$shared/lib/session"; + import UiWorkbench from "$app/pages/ui-workbench.svelte"; + import NotFound from "$app/pages/not-found.svelte"; + import Home from "$app/pages/home.svelte"; + import Settings from "$app/pages/settings.svelte"; + import Data from "$app/pages/data.svelte"; + import PreHeader from "$shared/components/pre-header.svelte"; + import {setLocale} from "$app/lib/i18n/i18n-svelte"; + import {loadLocaleAsync} from "$app/lib/i18n/i18n-util.async"; + import {i18nObject} from "$app/lib/i18n/i18n-util"; - let online = true; + let online = true; + let notOnlineText; + let LL; - async function user_is_logged_in() { - if (!await is_active()) { - await logout_user("expired"); - } - return true; - } + console.log("Projects Startup Report", { + prefferedLocale: navigator.language, + timeZone: Temporal.Now.timeZone().id, + go_theme: get_cookie(CookieNames.theme), + go_locale: get_cookie(CookieNames.locale), + prefersColorScheme: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" + }); - const queryClient = new QueryClient(); - - const routes = { - "/home": wrap({ - component: Home, - conditions: [user_is_logged_in], - }), - "/": wrap({ - component: Home, - conditions: [user_is_logged_in], - }), - "/settings": wrap({ - component: Settings, - conditions: [user_is_logged_in], - }), - "/data": wrap({ - component: Data, - conditions: [user_is_logged_in], - }), - "/ui-workbench": UiWorkbench, - "*": NotFound, - }; + currentLocale.subscribe(async locale => { + locale = locale === "preffered" ? preffered_or_default() : locale; + await loadLocaleAsync(locale); + LL = i18nObject(locale); + setLocale(locale); + }); + + onMount(async () => { + const locale = $currentLocale === "preffered" ? preffered_or_default() : $currentLocale; + await loadLocaleAsync(locale); + LL = i18nObject(locale); + setLocale(locale); + notOnlineText = LL.messages.noInternet(); + }); + + async function user_is_logged_in() { + if (!await is_active()) { + await logout_user("expired"); + } + return true; + } + + const queryClient = new QueryClient(); + + const routes = { + "/home": wrap({ + component: Home, + conditions: [user_is_logged_in], + }), + "/": wrap({ + component: Home, + conditions: [user_is_logged_in], + }), + "/settings": wrap({ + component: Settings, + conditions: [user_is_logged_in], + }), + "/data": wrap({ + component: Data, + conditions: [user_is_logged_in], + }), + "/ui-workbench": UiWorkbench, + "*": NotFound, + }; </script> -<PreHeader show="{!online}">You seem to be offline, please check your internet connection.</PreHeader> +<PreHeader show="{!online}">{notOnlineText}</PreHeader> <QueryClientProvider client={queryClient}> <Router diff --git a/apps/projects/src/app/lib/i18n/en/index.ts b/apps/projects/src/app/lib/i18n/en/index.ts new file mode 100644 index 0000000..9d74481 --- /dev/null +++ b/apps/projects/src/app/lib/i18n/en/index.ts @@ -0,0 +1,127 @@ +import type {BaseTranslation} from "../i18n-types"; + +const en: BaseTranslation = { + nav: { + home: "Home", + data: "Data", + settings: "Settings", + usermenu: { + logout: "Log out", + logoutTitle: "Log out of your profile", + profile: "Profile", + profileTitle: "Administrate your profile", + toggleTitle: "Toggle user menu", + } + }, + views: { + dataTablePaginator: { + goToPrevPage: "Go to previous page", + goToNextPage: "Go to next page", + of: "of", + }, + categoryForm: { + name: "Name", + color: "Color", + defaultLabels: "Default labels", + labelsPlaceholder: "Search or create" + }, + settingsCategoriesTile: { + deleteAllConfirm: "Are you sure you want to delete this category?\nThis will delete all relating entries!", + active: "Active", + archived: "Archived", + name: "Name", + color: "Color", + editEntry: "Edit entry", + deleteEntry: "Delete entry", + noCategories: "No categories", + categories: "Categories" + }, + settingsLabelsTile: { + deleteAllConfirm: "Are you sure you want to delete this label?\nIt will be removed from all related entries!", + active: "Active", + archived: "Archived", + name: "Name", + color: "Color", + editEntry: "Edit label", + deleteEntry: "Delete label", + noLabels: "No labels", + labels: "Labels" + }, + entryForm: { + entryUpdateError: "An error occured while updating the entry, try again soon.", + entryCreateError: "An error occured while creating the entry, try again soon.", + errDescriptionReq: "Description is required", + reset: "Reset", + description: "Description", + save: "Save", + create: "Create", + category: { + category: "Category", + placeholder: "Search or create", + noResults: "No categories available (Create a new one by searching for it)", + errisRequired: "Category is required", + _logReset: "Reset category section" + }, + labels: { + placeholder: "Search or create", + noResults: "No labels available (Create a new one by searching for it)", + labels: "Labels", + _logReset: "Reset labels section" + }, + dateTime: { + errDateIsRequired: "Date is required", + errFromIsRequired: "From is required", + errFromAfterTo: "From can not be after To", + errFromEqTo: "From and To can not be equal", + errToIsRequired: "To is required", + errToBeforeFrom: "To can not be before From", + from: "From", + to: "To", + date: "Date", + _logReset: "Reset date time section" + } + } + }, + data: { + durationSummary: "Showing {entryCountString:string}, totalling in {totalHourMin:string}", + hourSingleChar: "h", + minSingleChar: "m", + entry: "entry", + entries: "entries", + confirmDeleteEntry: "Are you sure you want to delete this entry?", + editEntry: "Edit entry", + date: "Date", + from: "From", + duration: "Duration", + category: "Category", + description: "Description", + loading: "Loading", + noEntries: "No entries", + to: "to", + use: "Use", + }, + home: { + hourSingleChar: "h", + minSingleChar: "m", + confirmDeleteEntry: "Are you sure you want to delete this entry?", + newEntry: "New entry", + editEntry: "Edit entry", + deleteEntry: "Delete entry", + loggedTimeToday: "Logged time today", + currentTime: "Current time", + loading: "Loading", + stopwatch: "Stopwatch", + todayEntries: "Today's entries", + noEntriesToday: "No entries today", + refreshTodayEntries: "Refresh today's entries", + category: "Category", + timespan: "Timespan", + }, + messages: { + pageNotFound: "Page not found", + goToFrontpage: "Go to frontpage", + noInternet: "It seems like your device does not have a internet connection, please check your connection." + } +}; + +export default en; diff --git a/apps/projects/src/app/lib/i18n/formatters.ts b/apps/projects/src/app/lib/i18n/formatters.ts new file mode 100644 index 0000000..78734f9 --- /dev/null +++ b/apps/projects/src/app/lib/i18n/formatters.ts @@ -0,0 +1,11 @@ +import type { FormattersInitializer } from 'typesafe-i18n' +import type { Locales, Formatters } from './i18n-types' + +export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => { + + const formatters: Formatters = { + // add your formatter functions here + } + + return formatters +} diff --git a/apps/projects/src/app/lib/i18n/i18n-svelte.ts b/apps/projects/src/app/lib/i18n/i18n-svelte.ts new file mode 100644 index 0000000..6cdffb3 --- /dev/null +++ b/apps/projects/src/app/lib/i18n/i18n-svelte.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initI18nSvelte } from 'typesafe-i18n/svelte' +import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales } from './i18n-util' + +const { locale, LL, setLocale } = initI18nSvelte<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters) + +export { locale, LL, setLocale } + +export default LL diff --git a/apps/projects/src/app/lib/i18n/i18n-types.ts b/apps/projects/src/app/lib/i18n/i18n-types.ts new file mode 100644 index 0000000..f9fd9cc --- /dev/null +++ b/apps/projects/src/app/lib/i18n/i18n-types.ts @@ -0,0 +1,812 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ +import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n' + +export type BaseTranslation = BaseTranslationType +export type BaseLocale = 'en' + +export type Locales = + | 'en' + | 'nb' + +export type Translation = RootTranslation + +export type Translations = RootTranslation + +type RootTranslation = { + nav: { + /** + * Home + */ + home: string + /** + * Data + */ + data: string + /** + * Settings + */ + settings: string + usermenu: { + /** + * Log out + */ + logout: string + /** + * Log out of your profile + */ + logoutTitle: string + /** + * Profile + */ + profile: string + /** + * Administrate your profile + */ + profileTitle: string + /** + * Toggle user menu + */ + toggleTitle: string + } + } + views: { + dataTablePaginator: { + /** + * Go to previous page + */ + goToPrevPage: string + /** + * Go to next page + */ + goToNextPage: string + /** + * of + */ + of: string + } + categoryForm: { + /** + * Name + */ + name: string + /** + * Color + */ + color: string + /** + * Default labels + */ + defaultLabels: string + /** + * Search or create + */ + labelsPlaceholder: string + } + settingsCategoriesTile: { + /** + * Are you sure you want to delete this category? + This will delete all relating entries! + */ + deleteAllConfirm: string + /** + * Active + */ + active: string + /** + * Archived + */ + archived: string + /** + * Name + */ + name: string + /** + * Color + */ + color: string + /** + * Edit entry + */ + editEntry: string + /** + * Delete entry + */ + deleteEntry: string + /** + * No categories + */ + noCategories: string + } + settingsLabelsTile: { + /** + * Are you sure you want to delete this label? + It will be removed from all related entries! + */ + deleteAllConfirm: string + /** + * Active + */ + active: string + /** + * Archived + */ + archived: string + /** + * Name + */ + name: string + /** + * Color + */ + color: string + /** + * Edit label + */ + editEntry: string + /** + * Delete label + */ + deleteEntry: string + /** + * No labels + */ + noLabels: string + } + entryForm: { + /** + * An error occured while updating the entry, try again soon. + */ + entryUpdateError: string + /** + * An error occured while creating the entry, try again soon. + */ + entryCreateError: string + /** + * Description is required + */ + errDescriptionReq: string + /** + * Reset + */ + reset: string + /** + * Description + */ + description: string + /** + * Save + */ + save: string + /** + * Create + */ + create: string + category: { + /** + * Category + */ + category: string + /** + * Search or create + */ + placeholder: string + /** + * No categories available (Create a new one by searching for it) + */ + noResults: string + /** + * Category is required + */ + errisRequired: string + /** + * Reset category section + */ + _logReset: string + } + labels: { + /** + * Search or create + */ + placeholder: string + /** + * No labels available (Create a new one by searching for it) + */ + noResults: string + /** + * Labels + */ + labels: string + /** + * Reset labels section + */ + _logReset: string + } + dateTime: { + /** + * Date is required + */ + errDateIsRequired: string + /** + * From is required + */ + errFromIsRequired: string + /** + * From can not be after To + */ + errFromAfterTo: string + /** + * From and To can not be equal + */ + errFromEqTo: string + /** + * To is required + */ + errToIsRequired: string + /** + * To can not be before From + */ + errToBeforeFrom: string + /** + * From + */ + from: string + /** + * To + */ + to: string + /** + * Date + */ + date: string + /** + * Reset date time section + */ + _logReset: string + } + } + } + data: { + /** + * Showing {entryCountString}, totalling in {totalHourMin} + * @param {string} entryCountString + * @param {string} totalHourMin + */ + durationSummary: RequiredParams<'entryCountString' | 'totalHourMin'> + /** + * h + */ + hourSingleChar: string + /** + * m + */ + minSingleChar: string + /** + * entry + */ + entry: string + /** + * entries + */ + entries: string + /** + * Are you sure you want to delete this entry? + */ + confirmDeleteEntry: string + /** + * Edit entry + */ + editEntry: string + /** + * Date + */ + date: string + /** + * From + */ + from: string + /** + * Duration + */ + duration: string + /** + * Category + */ + category: string + /** + * Description + */ + description: string + /** + * Loading + */ + loading: string + /** + * No entries + */ + noEntries: string + /** + * to + */ + to: string + /** + * Use + */ + use: string + } + home: { + /** + * h + */ + hourSingleChar: string + /** + * m + */ + minSingleChar: string + /** + * Are you sure you want to delete this entry? + */ + confirmDeleteEntry: string + /** + * New entry + */ + newEntry: string + /** + * Edit entry + */ + editEntry: string + /** + * Delete entry + */ + deleteEntry: string + /** + * Logged time today + */ + loggedTimeToday: string + /** + * Current time + */ + currentTime: string + /** + * Loading + */ + loading: string + /** + * Stopwatch + */ + stopwatch: string + /** + * Today's entries + */ + todayEntries: string + /** + * No entries today + */ + noEntriesToday: string + /** + * Refresh today's entries + */ + refreshTodayEntries: string + /** + * Category + */ + category: string + /** + * Timespan + */ + timespan: string + } + messages: { + /** + * Page not found + */ + pageNotFound: string + /** + * Go to frontpage + */ + goToFrontpage: string + /** + * It seems like your device does not have a internet connection, please check your connection. + */ + noInternet: string + } +} + +export type TranslationFunctions = { + nav: { + /** + * Home + */ + home: () => LocalizedString + /** + * Data + */ + data: () => LocalizedString + /** + * Settings + */ + settings: () => LocalizedString + usermenu: { + /** + * Log out + */ + logout: () => LocalizedString + /** + * Log out of your profile + */ + logoutTitle: () => LocalizedString + /** + * Profile + */ + profile: () => LocalizedString + /** + * Administrate your profile + */ + profileTitle: () => LocalizedString + /** + * Toggle user menu + */ + toggleTitle: () => LocalizedString + } + } + views: { + dataTablePaginator: { + /** + * Go to previous page + */ + goToPrevPage: () => LocalizedString + /** + * Go to next page + */ + goToNextPage: () => LocalizedString + /** + * of + */ + of: () => LocalizedString + } + categoryForm: { + /** + * Name + */ + name: () => LocalizedString + /** + * Color + */ + color: () => LocalizedString + /** + * Default labels + */ + defaultLabels: () => LocalizedString + /** + * Search or create + */ + labelsPlaceholder: () => LocalizedString + } + settingsCategoriesTile: { + /** + * Are you sure you want to delete this category? + This will delete all relating entries! + */ + deleteAllConfirm: () => LocalizedString + /** + * Active + */ + active: () => LocalizedString + /** + * Archived + */ + archived: () => LocalizedString + /** + * Name + */ + name: () => LocalizedString + /** + * Color + */ + color: () => LocalizedString + /** + * Edit entry + */ + editEntry: () => LocalizedString + /** + * Delete entry + */ + deleteEntry: () => LocalizedString + /** + * No categories + */ + noCategories: () => LocalizedString + } + settingsLabelsTile: { + /** + * Are you sure you want to delete this label? + It will be removed from all related entries! + */ + deleteAllConfirm: () => LocalizedString + /** + * Active + */ + active: () => LocalizedString + /** + * Archived + */ + archived: () => LocalizedString + /** + * Name + */ + name: () => LocalizedString + /** + * Color + */ + color: () => LocalizedString + /** + * Edit label + */ + editEntry: () => LocalizedString + /** + * Delete label + */ + deleteEntry: () => LocalizedString + /** + * No labels + */ + noLabels: () => LocalizedString + } + entryForm: { + /** + * An error occured while updating the entry, try again soon. + */ + entryUpdateError: () => LocalizedString + /** + * An error occured while creating the entry, try again soon. + */ + entryCreateError: () => LocalizedString + /** + * Description is required + */ + errDescriptionReq: () => LocalizedString + /** + * Reset + */ + reset: () => LocalizedString + /** + * Description + */ + description: () => LocalizedString + /** + * Save + */ + save: () => LocalizedString + /** + * Create + */ + create: () => LocalizedString + category: { + /** + * Category + */ + category: () => LocalizedString + /** + * Search or create + */ + placeholder: () => LocalizedString + /** + * No categories available (Create a new one by searching for it) + */ + noResults: () => LocalizedString + /** + * Category is required + */ + errisRequired: () => LocalizedString + /** + * Reset category section + */ + _logReset: () => LocalizedString + } + labels: { + /** + * Search or create + */ + placeholder: () => LocalizedString + /** + * No labels available (Create a new one by searching for it) + */ + noResults: () => LocalizedString + /** + * Labels + */ + labels: () => LocalizedString + /** + * Reset labels section + */ + _logReset: () => LocalizedString + } + dateTime: { + /** + * Date is required + */ + errDateIsRequired: () => LocalizedString + /** + * From is required + */ + errFromIsRequired: () => LocalizedString + /** + * From can not be after To + */ + errFromAfterTo: () => LocalizedString + /** + * From and To can not be equal + */ + errFromEqTo: () => LocalizedString + /** + * To is required + */ + errToIsRequired: () => LocalizedString + /** + * To can not be before From + */ + errToBeforeFrom: () => LocalizedString + /** + * From + */ + from: () => LocalizedString + /** + * To + */ + to: () => LocalizedString + /** + * Date + */ + date: () => LocalizedString + /** + * Reset date time section + */ + _logReset: () => LocalizedString + } + } + } + data: { + /** + * Showing {entryCountString}, totalling in {totalHourMin} + */ + durationSummary: (arg: { entryCountString: string, totalHourMin: string }) => LocalizedString + /** + * h + */ + hourSingleChar: () => LocalizedString + /** + * m + */ + minSingleChar: () => LocalizedString + /** + * entry + */ + entry: () => LocalizedString + /** + * entries + */ + entries: () => LocalizedString + /** + * Are you sure you want to delete this entry? + */ + confirmDeleteEntry: () => LocalizedString + /** + * Edit entry + */ + editEntry: () => LocalizedString + /** + * Date + */ + date: () => LocalizedString + /** + * From + */ + from: () => LocalizedString + /** + * Duration + */ + duration: () => LocalizedString + /** + * Category + */ + category: () => LocalizedString + /** + * Description + */ + description: () => LocalizedString + /** + * Loading + */ + loading: () => LocalizedString + /** + * No entries + */ + noEntries: () => LocalizedString + /** + * to + */ + to: () => LocalizedString + /** + * Use + */ + use: () => LocalizedString + } + home: { + /** + * h + */ + hourSingleChar: () => LocalizedString + /** + * m + */ + minSingleChar: () => LocalizedString + /** + * Are you sure you want to delete this entry? + */ + confirmDeleteEntry: () => LocalizedString + /** + * New entry + */ + newEntry: () => LocalizedString + /** + * Edit entry + */ + editEntry: () => LocalizedString + /** + * Delete entry + */ + deleteEntry: () => LocalizedString + /** + * Logged time today + */ + loggedTimeToday: () => LocalizedString + /** + * Current time + */ + currentTime: () => LocalizedString + /** + * Loading + */ + loading: () => LocalizedString + /** + * Stopwatch + */ + stopwatch: () => LocalizedString + /** + * Today's entries + */ + todayEntries: () => LocalizedString + /** + * No entries today + */ + noEntriesToday: () => LocalizedString + /** + * Refresh today's entries + */ + refreshTodayEntries: () => LocalizedString + /** + * Category + */ + category: () => LocalizedString + /** + * Timespan + */ + timespan: () => LocalizedString + } + messages: { + /** + * Page not found + */ + pageNotFound: () => LocalizedString + /** + * Go to frontpage + */ + goToFrontpage: () => LocalizedString + /** + * It seems like your device does not have a internet connection, please check your connection. + */ + noInternet: () => LocalizedString + } +} + +export type Formatters = {} diff --git a/apps/projects/src/app/lib/i18n/i18n-util.async.ts b/apps/projects/src/app/lib/i18n/i18n-util.async.ts new file mode 100644 index 0000000..75b90c9 --- /dev/null +++ b/apps/projects/src/app/lib/i18n/i18n-util.async.ts @@ -0,0 +1,27 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initFormatters } from './formatters' +import type { Locales, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales, locales } from './i18n-util' + +const localeTranslationLoaders = { + en: () => import('./en'), + nb: () => import('./nb'), +} + +const updateDictionary = (locale: Locales, dictionary: Partial<Translations>) => + loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary } + +export const loadLocaleAsync = async (locale: Locales): Promise<void> => { + updateDictionary( + locale, + (await localeTranslationLoaders[locale]()).default as unknown as Translations + ) + loadFormatters(locale) +} + +export const loadAllLocalesAsync = (): Promise<void[]> => Promise.all(locales.map(loadLocaleAsync)) + +export const loadFormatters = (locale: Locales): void => + void (loadedFormatters[locale] = initFormatters(locale)) diff --git a/apps/projects/src/app/lib/i18n/i18n-util.sync.ts b/apps/projects/src/app/lib/i18n/i18n-util.sync.ts new file mode 100644 index 0000000..7a1d51e --- /dev/null +++ b/apps/projects/src/app/lib/i18n/i18n-util.sync.ts @@ -0,0 +1,27 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initFormatters } from './formatters' +import type { Locales, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales, locales } from './i18n-util' + +import en from './en' +import nb from './nb' + +const localeTranslations = { + en, + nb, +} + +export const loadLocale = (locale: Locales): void => { + if (loadedLocales[locale]) return + + loadedLocales[locale] = localeTranslations[locale] as unknown as Translations + loadFormatters(locale) +} + +export const loadAllLocales = (): void => locales.forEach(loadLocale) + +export const loadFormatters = (locale: Locales): void => { + loadedFormatters[locale] = initFormatters(locale) +} diff --git a/apps/projects/src/app/lib/i18n/i18n-util.ts b/apps/projects/src/app/lib/i18n/i18n-util.ts new file mode 100644 index 0000000..cad1e7a --- /dev/null +++ b/apps/projects/src/app/lib/i18n/i18n-util.ts @@ -0,0 +1,31 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n' +import type { LocaleDetector } from 'typesafe-i18n/detectors' +import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors' +import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types' + +export const baseLocale: Locales = 'en' + +export const locales: Locales[] = [ + 'en', + 'nb' +] + +export const loadedLocales = {} as Record<Locales, Translations> + +export const loadedFormatters = {} as Record<Locales, Formatters> + +export const i18nString = (locale: Locales) => initI18nString<Locales, Formatters>(locale, loadedFormatters[locale]) + +export const i18nObject = (locale: Locales) => + initI18nObject<Locales, Translations, TranslationFunctions, Formatters>( + locale, + loadedLocales[locale], + loadedFormatters[locale] + ) + +export const i18n = () => initI18n<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters) + +export const detectLocale = (...detectors: LocaleDetector[]) => detectLocaleFn<Locales>(baseLocale, locales, ...detectors) diff --git a/apps/projects/src/app/lib/i18n/nb/index.ts b/apps/projects/src/app/lib/i18n/nb/index.ts new file mode 100644 index 0000000..af3a487 --- /dev/null +++ b/apps/projects/src/app/lib/i18n/nb/index.ts @@ -0,0 +1,127 @@ +import type {Translation} from "../i18n-types"; + +const nb: Translation = { + nav: { + home: "Hjem", + data: "Data", + settings: "Innstillinger", + usermenu: { + logout: "Logg ut", + logoutTitle: "Logg ut av din profil", + profile: "Profil", + profileTitle: "Administrer din profil", + toggleTitle: "Vis brukermeny" + } + }, + views: { + categoryForm: { + name: "Navn", + color: "Farge", + defaultLabels: "Standard merknader", + labelsPlaceholder: "Søk eller opprett" + }, + dataTablePaginator: { + goToPrevPage: "Gå til forrige side", + goToNextPage: "Gå til neste side", + of: "av", + }, + settingsCategoriesTile: { + deleteAllConfirm: "Er du sikker på at du vil slette denne kategorien?\nDette vil slette alle tilhørende rader!", + active: "Aktive", + archived: "Arkiverte", + name: "Navn", + color: "Farge", + editEntry: "Rediger kategori", + deleteEntry: "Slett kategori", + noCategories: "Ingen kategorier", + categories: "Kategorier" + }, + settingsLabelsTile: { + deleteAllConfirm: "Er du sikker på at du vil slette denne merknaden?\nDen vil bli slette fra alle relaterte rader!", + active: "Aktive", + archived: "Arkiverte", + name: "Navn", + color: "Farge", + editEntry: "Rediger merknad", + deleteEntry: "Slett merknad", + noLabels: "Ingen merknader", + labels: "Merknader" + }, + entryForm: { + entryUpdateError: "En feil oppstod med lagringen av din rad, prøv igjen snart.", + entryCreateError: "En feil oppstod med opprettelsen av din rad, prøv igjen snart.", + errDescriptionReq: "Beskrivelse er påkrevd", + reset: "Tilbakestill", + description: "Beskrivelse", + save: "Lagre", + create: "Opprett", + category: { + category: "Kategori", + placeholder: "Søk eller opprett", + noResults: "Ingen kategorier tilgjengelig (Opprett en ny ved å skrive navnet i søkefeltet).", + errisRequired: "Kategori er påkrevd", + _logReset: "Tilbakestilte kategori-seksjonen" + }, + labels: { + placeholder: "Søk eller opprett", + noResults: "Ingen merkander tilgjengelig (Opprett en ny ved å skrive navnet i søkefeltet).", + labels: "Merknader", + _logReset: "Tilbakestilte merknader-seksjonen" + }, + dateTime: { + errDateIsRequired: "Dato er påkrevd", + errFromIsRequired: "Fra er påkrevd", + errFromAfterTo: "Fra kan ikke være etter Til", + errFromEqTo: "Fra og Til kan ikke ha lik verdi", + errToIsRequired: "Til er påkrevd", + errToBeforeFrom: "Til kan ikke være før Fra", + from: "Fra", + to: "Til", + date: "Dato", + _logReset: "Tilbakestilte dato-seksjonen" + } + } + }, + data: { + durationSummary: "Viser {entryCountString:string}, Tilsammen {totalHourMin:string}", + hourSingleChar: "t", + minSingleChar: "m", + entry: "rad", + entries: "rader", + confirmDeleteEntry: "Er du sikker på at du vil slette denne raden?", + editEntry: "Rediger rad", + date: "Dato", + from: "Fra", + duration: "Tidsrom", + category: "Kategori", + description: "Beskrivelse", + loading: "Laster", + noEntries: "Ingen rader", + to: "til", + use: "Bruk", + }, + home: { + hourSingleChar: "t", + minSingleChar: "m", + confirmDeleteEntry: "Er du sikker på at du vil slette denne raden?", + newEntry: "Ny rad", + editEntry: "Rediger rad", + deleteEntry: "Slett rad", + loggedTimeToday: "Registrert tid hittil idag", + currentTime: "Klokken", + loading: "Laster", + stopwatch: "Stoppeklokke", + todayEntries: "Dagens rader", + noEntriesToday: "Ingen rader i dag", + refreshTodayEntries: "Last inn dagens rader på nytt", + category: "Kategori", + timespan: "Tidsrom", + }, + messages: { + pageNotFound: "Fant ikke siden", + goToFrontpage: "Gå til forsiden", + noInternet: "Det ser ut som at du er uten internettilgang, vennligst sjekk tilkoblingen din." + } +}; + +export default nb; diff --git a/apps/projects/src/app/lib/stores/locale.ts b/apps/projects/src/app/lib/stores/locale.ts new file mode 100644 index 0000000..1215c20 --- /dev/null +++ b/apps/projects/src/app/lib/stores/locale.ts @@ -0,0 +1,21 @@ +import {base_domain, CookieNames} from "$shared/lib/configuration"; +import {get_cookie, set_cookie} from "$shared/lib/helpers"; +import {writable} from "svelte/store"; +import type {Locales} from "$app/lib/i18n/i18n-types"; + +export function preffered_or_default(): Locales { + if (/^en\b/i.test(navigator.language)) { + return "en"; + } + if (/^nb\b/i.test(navigator.language) || /^nn\b/i.test(navigator.language)) { + return "nb"; + } + return "en"; +} + +export const currentLocale = writable<Locales>((get_cookie(CookieNames.locale) ?? preffered_or_default()) as Locales); +currentLocale.subscribe(locale => { + //@ts-ignore + if (locale === "preffered") set_cookie(CookieNames.locale, preffered_or_default(), base_domain()); + set_cookie(CookieNames.locale, locale, base_domain()); +}); diff --git a/apps/projects/src/app/pages/_layout.svelte b/apps/projects/src/app/pages/_layout.svelte index 3d632ae..fb34593 100644 --- a/apps/projects/src/app/pages/_layout.svelte +++ b/apps/projects/src/app/pages/_layout.svelte @@ -2,18 +2,27 @@ import {onMount} from "svelte"; import {location, link} from "svelte-spa-router"; import {logout_user} from "$app/lib/services/user-service"; - import {random_string, switch_theme} from "$shared/lib/helpers"; + import {random_string} from "$shared/lib/helpers"; import {get_session_data} from "$shared/lib/session"; import ProfileModal from "$app/pages/views/profile-modal.svelte"; import {Menu, MenuItem, MenuItemSeparator} from "$shared/components/menu"; import Button from "$shared/components/button.svelte"; import {IconNames} from "$shared/lib/configuration"; + import LL from "$app/lib/i18n/i18n-svelte"; + import BlowoutToolbelt from "$shared/components/blowout-toolbelt.svelte"; + import {currentLocale} from "$app/lib/stores/locale"; let ProfileModalFunctions = {}; let showUserMenu = false; let userMenuTriggerNode; const userMenuId = "__menu_" + random_string(3); - const username = get_session_data().profile.username; + const username = get_session_data()?.profile.username; + + function toolbelt_change(event) { + if (event.detail.name === "locale") { + currentLocale.set(event.detail.value); + } + } onMount(() => { userMenuTriggerNode = document.getElementById("open-user-menu"); @@ -21,6 +30,7 @@ </script> <ProfileModal bind:functions={ProfileModalFunctions}/> +<BlowoutToolbelt on:change={toolbelt_change}/> <nav class="container max-width-xl@md width-fit-content@md width-100% max-width-none margin-y-xs@md margin-bottom-xs block@md position-relative@md position-absolute bottom-unset@md bottom-0"> <div class="tabs-nav-v2 justify-between"> @@ -28,17 +38,17 @@ <div class="tab-v2"> <a href="/home" use:link - class="tabs-nav-v2__item {($location === '/' || $location.startsWith('/home')) ? 'tabs-nav-v2__item--selected' : ''}">Home</a> + class="tabs-nav-v2__item {($location === '/' || $location.startsWith('/home')) ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.home()}</a> </div> <div class="tab-v2"> <a href="/data" use:link - class="tabs-nav-v2__item {$location.startsWith('/data') ? 'tabs-nav-v2__item--selected' : ''}">Data</a> + class="tabs-nav-v2__item {$location.startsWith('/data') ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.data()}</a> </div> <div class="tab-v2"> <a href="/settings" use:link - class="tabs-nav-v2__item {$location.startsWith('/settings') ? 'tabs-nav-v2__item--selected' : ''}">Settings</a> + class="tabs-nav-v2__item {$location.startsWith('/settings') ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.settings()}</a> </div> </div> <div class="tab-v2 padding-x-sm"> @@ -51,7 +61,7 @@ icon_width="2rem" icon_height="2rem" icon_right_aligned="true" - title="Toggle user menu" + title="{$LL.nav.usermenu.toggleTitle()}" aria-controls="{userMenuId}" /> <Menu bind:show="{showUserMenu}" @@ -59,14 +69,12 @@ id="{userMenuId}"> <div slot="options"> <MenuItem on:click={() => ProfileModalFunctions.open()}> - <span title="Administrate your profile">Profile</span> - </MenuItem> - <MenuItem on:click={() => switch_theme()}> - <span title="Change between a dark and light theme">Switch theme</span> + <span title="{$LL.nav.usermenu.profileTitle()}">{$LL.nav.usermenu.profile()}</span> </MenuItem> <MenuItemSeparator/> - <MenuItem danger="true" on:click={() => logout_user()}> - <span title="Log out of your profile">Log out</span> + <MenuItem danger="true" + on:click={() => logout_user()}> + <span title="{$LL.nav.usermenu.logoutTitle()}">{$LL.nav.usermenu.logout()}</span> </MenuItem> </div> </Menu> diff --git a/apps/projects/src/app/pages/data.svelte b/apps/projects/src/app/pages/data.svelte index 070b98b..190c641 100644 --- a/apps/projects/src/app/pages/data.svelte +++ b/apps/projects/src/app/pages/data.svelte @@ -12,6 +12,7 @@ import {delete_time_entry, get_time_entries, get_time_entry} from "$shared/lib/api/time-entry"; import {seconds_to_hour_minute_string, is_guid, move_focus, unwrap_date_time_from_entry} from "$shared/lib/helpers"; import Button from "$shared/components/button.svelte"; + import LL from "$app/lib/i18n/i18n-svelte"; let pageCount = 1; let page = 1; @@ -41,7 +42,10 @@ function set_duration_summary_string() { if (entries.length > 0) { - durationSummary = `Showing ${entries.length} ${entries.length === 1 ? "entry" : "entries"}, totalling in ${seconds_to_hour_minute_string(secondsLogged)}`; + durationSummary = $LL.data.durationSummary({ + entryCountString: `${entries.length} ${entries.length === 1 ? $LL.data.entry() : $LL.data.entries()}`, + totalHourMin: seconds_to_hour_minute_string(secondsLogged) + }); } else { durationSummary = ""; } @@ -61,7 +65,7 @@ date: date_time.start_date, start: date_time.start_time, stop: date_time.stop_time, - durationString: date_time.duration.hours + "h" + date_time.duration.minutes + "m", + durationString: date_time.duration.hours + $LL.data.hourSingleChar() + date_time.duration.minutes + $LL.data.minSingleChar(), seconds: seconds, category: entry.category, labels: entry.labels, @@ -126,7 +130,7 @@ } async function handle_delete_entry_button_click(e, entryId) { - if (confirm("Are you sure you want to delete this entry?")) { + if (confirm($LL.data.confirmDeleteEntry())) { const response = await delete_time_entry(entryId); if (response.ok) { const indexOfEntry = entries.findIndex((c) => c.id === entryId); @@ -180,7 +184,7 @@ }); </script> -<Modal title="Edit entry" +<Modal title="{$LL.data.editEntry()}" bind:functions={EditEntryModal} on:closed={() => EditEntryForm.reset()}> <EntryForm bind:functions={EditEntryForm} @@ -216,7 +220,7 @@ {#if currentTimespanFilter === TimeEntryQueryDuration.SPECIFIC_DATE} <div class="flex items-baseline margin-bottom-xxxxs justify-between"> - <span class="text-sm color-contrast-medium margin-right-xs">Date:</span> + <span class="text-sm color-contrast-medium margin-right-xs">{$LL.data.date()}:</span> <span class="text-sm"> <input type="date" class="border-none padding-0 color-inherit bg-transparent" @@ -227,7 +231,7 @@ {#if currentTimespanFilter === TimeEntryQueryDuration.DATE_RANGE} <div class="flex items-baseline margin-bottom-xxxxs justify-between"> - <span class="text-sm color-contrast-medium margin-right-xs">From:</span> + <span class="text-sm color-contrast-medium margin-right-xs">{$LL.data.from()}:</span> <span class="text-sm"> <input type="date" class="border-none padding-0 color-inherit bg-transparent" @@ -236,7 +240,7 @@ </div> <div class="flex items-baseline margin-bottom-xxxxs justify-between"> - <span class="text-sm color-contrast-medium margin-right-xs">To:</span> + <span class="text-sm color-contrast-medium margin-right-xs">{$LL.data.to()}:</span> <span class="text-sm"> <input type="date" class="border-none padding-0 color-inherit bg-transparent" @@ -249,13 +253,13 @@ <Button variant="subtle" on:click={() => load_entries_with_filter(page)} class="text-sm" - text="Save"/> + text="{$LL.data.use()}"/> </div> </div> <Layout> <Tile class="{isLoading ? 'c-disabled loading' : ''}"> - <nav class="s-tabs text-sm"> + <nav class="s-tabs text-sm hide"> <ul class="s-tabs__list"> <li><span class="s-tabs__link s-tabs__link--current">All (21)</span></li> <li><span class="s-tabs__link">Published (19)</span></li> @@ -280,7 +284,7 @@ <TCell type="th" style="width: 100px"> <div class="flex items-center justify-between"> - <span>Date</span> + <span>{$LL.data.date()}</span> <div class="date_filter_box_el cursor-pointer" on:click={toggle_date_filter_box}> <Icon name="{IconNames.funnel}"/> @@ -291,21 +295,21 @@ <TCell type="th" style="width: 100px"> <div class="flex items-center"> - <span>Duration</span> + <span>{$LL.data.duration()}</span> </div> </TCell> <TCell type="th" style="width: 100px;"> <div class="flex items-center"> - <span>Category</span> + <span>{$LL.data.category()}</span> </div> </TCell> <TCell type="th" style="width: 300px;"> <div class="flex items-center"> - <span>Description</span> + <span>{$LL.data.description()}</span> </div> </TCell> <TCell type="th" @@ -366,7 +370,7 @@ <TCell type="th" thScope="row" colspan="7"> - {isLoading ? "Loading..." : "No entries"} + {isLoading ? $LL.data.loading() + "..." : $LL.data.noEntries()} </TCell> </TRow> {/if} @@ -378,7 +382,7 @@ {#if durationSummary} <small class={isLoading ? "c-disabled loading" : ""}>{durationSummary}</small> {:else} - <small class={isLoading ? "c-disabled loading" : ""}>No entries</small> + <small class={isLoading ? "c-disabled loading" : ""}>{$LL.data.noEntries()}</small> {/if} </p> diff --git a/apps/projects/src/app/pages/home.svelte b/apps/projects/src/app/pages/home.svelte index 84d6728..33bb0d8 100644 --- a/apps/projects/src/app/pages/home.svelte +++ b/apps/projects/src/app/pages/home.svelte @@ -1,4 +1,5 @@ <script lang="ts"> + import LL from "$app/lib/i18n/i18n-svelte"; import {delete_time_entry, get_time_entries, get_time_entry, update_time_entry} from "$shared/lib/api/time-entry"; import {IconNames, QueryKeys} from "$shared/lib/configuration"; import {TimeEntryDto} from "$shared/lib/models/TimeEntryDto"; @@ -18,7 +19,7 @@ let isLoading = false; let EditEntryForm: any; let timeEntries = [] as Array<TimeEntryDto>; - let timeLoggedTodayString = "0h0m"; + let timeLoggedTodayString = "0" + $LL.home.hourSingleChar() + "0" + $LL.home.minSingleChar(); const queryClient = useQueryClient(); const queryResult = useQuery(QueryKeys.entries, async () => await get_time_entries({ @@ -49,7 +50,7 @@ } async function on_delete_entry_button_click(event, entryId: string) { - if (confirm("Are you sure you want to delete this entry?")) { + if (confirm($LL.home.confirmDeleteEntry())) { $delete_entry_mutation.mutate(entryId); } } @@ -88,34 +89,34 @@ <Layout> <div class="grid gap-md margin-top-xs flex-row@md items-start flex-column-reverse"> <Tile class="col"> - <h3 class="text-md padding-bottom-xxxs">New entry</h3> + <h3 class="text-md padding-bottom-xxxs">{$LL.home.newEntry()}</h3> <EntryFrom bind:functions={EditEntryForm}/> </Tile> <div class="col grid gap-sm"> <Tile class="col-6@md col-12"> <p class="text-xxl">{timeLoggedTodayString}</p> - <p class="text-xs margin-bottom-xxs">Logged time today</p> + <p class="text-xs margin-bottom-xxs">{$LL.home.loggedTimeToday()}</p> <pre class="text-xxl">{currentTime}</pre> - <p class="text-xs">Current time</p> + <p class="text-xs">{$LL.home.currentTime()}</p> </Tile> <Tile class="col-6@md col-12"> <Stopwatch on:create={on_create_from_stopwatch}> <h3 slot="header" - class="text-md">Stopwatch</h3> + class="text-md">{$LL.home.stopwatch()}</h3> </Stopwatch> </Tile> <Tile class="col-12"> - <h3 class="text-md padding-bottom-xxxs">Today's entries</h3> + <h3 class="text-md padding-bottom-xxxs">{$LL.home.todayEntries()}</h3> <div class="max-width-100% overflow-auto"> <Table class="width-100% text-sm"> <THead> <TCell type="th" class="text-left"> - <span>Category</span> + <span>{$LL.home.category()}</span> </TCell> <TCell type="th" class="text-left"> - <span>Timespan</span> + <span>{$LL.home.timespan()}</span> </TCell> <TCell type="th" class="text-right"> @@ -123,7 +124,7 @@ variant="reset" icon_width="1.2rem" icon_height="1.2rem" - title="Refresh today's entries" + title="{$LL.home.refreshTodayEntries()}" on:click={() => queryClient.invalidateQueries(QueryKeys.entries)}/> </TCell> </THead> @@ -148,13 +149,13 @@ icon_width="1.2rem" icon_height="1.2rem" on:click={(e) => on_edit_entry_button_click(e, entry.id)} - title="Edit entry"/> + title="{$LL.home.editEntry()}"/> <Button icon="{IconNames.trash}" variant="reset" icon_width="1.2rem" icon_height="1.2rem" on:click={(e) => on_delete_entry_button_click(e, entry.id)} - title="Delete entry"/> + title="{$LL.home.deleteEntry()}"/> </TCell> </TRow> {/each} @@ -163,7 +164,7 @@ <TCell type="th" thScope="row" colspan="7"> - {isLoading ? "Loading..." : "No entries today"} + {isLoading ? $LL.home.loading() + "..." : $LL.home.noEntriesToday()} </TCell> </TRow> {/if} diff --git a/apps/projects/src/app/pages/not-found.svelte b/apps/projects/src/app/pages/not-found.svelte index 46d0d1d..8822e0e 100644 --- a/apps/projects/src/app/pages/not-found.svelte +++ b/apps/projects/src/app/pages/not-found.svelte @@ -1,4 +1,5 @@ <script> + import LL from "$app/lib/i18n/i18n-svelte"; import {link} from "svelte-spa-router"; </script> @@ -18,7 +19,7 @@ <main> <header>404</header> - <p>Page not found!</p> + <p>{$LL.messages.pageNotFound()}</p> <a use:link - href="/">Go to front</a> + href="/">{$LL.messages.goToFrontpage()}</a> </main> diff --git a/apps/projects/src/app/pages/views/category-form/index.svelte b/apps/projects/src/app/pages/views/category-form/index.svelte index e8c0f94..21024c3 100644 --- a/apps/projects/src/app/pages/views/category-form/index.svelte +++ b/apps/projects/src/app/pages/views/category-form/index.svelte @@ -1,9 +1,9 @@ <script lang="ts"> import Alert from "$shared/components/alert.svelte"; import Dropdown from "$shared/components/dropdown.svelte"; - import labels, {reload_labels, create_label_async} from "$app/lib/stores/labels"; + import labels, {create_label_async} from "$app/lib/stores/labels"; import {generate_random_hex_color} from "$shared/lib/colors"; - import {get} from "svelte/store"; + import LL from "$app/lib/i18n/i18n-svelte"; let LabelsDropdown; @@ -106,7 +106,7 @@ <div class="grid gap-x-xs margin-bottom-sm"> <div class="col-10"> <label for="name" - class="form-label margin-bottom-xxs">Name</label> + class="form-label margin-bottom-xxs">{$LL.views.categoryForm.name()}</label> <input type="text" class="form-control width-100%" id="name" @@ -117,7 +117,7 @@ </div> <div class="col-2"> <label for="color" - class="form-label margin-bottom-xxs">Color</label> + class="form-label margin-bottom-xxs">{$LL.views.categoryForm.color()}</label> <input type="color" class="form-control width-100%" id="color" @@ -130,10 +130,10 @@ </div> <div class="margin-bottom-sm"> <label for="labels" - class="form-label margin-bottom-xxs">Default labels</label> + class="form-label margin-bottom-xxs">{$LL.views.categoryForm.defaultLabels()}</label> <Dropdown id="labels" createable={true} - placeholder="Search or create" + placeholder="{$LL.views.categoryForm.labelsPlaceholder()}" entries={$labels} multiple={true} on_create_async={(name) => dough.fields.labels.create({name})}/> diff --git a/apps/projects/src/app/pages/views/data-table-paginator.svelte b/apps/projects/src/app/pages/views/data-table-paginator.svelte index 3d9834a..b2649eb 100644 --- a/apps/projects/src/app/pages/views/data-table-paginator.svelte +++ b/apps/projects/src/app/pages/views/data-table-paginator.svelte @@ -1,4 +1,5 @@ <script> + import LL from "$app/lib/i18n/i18n-svelte"; import {createEventDispatcher, onMount} from "svelte"; import {restrict_input_to_numbers} from "$shared/lib/helpers"; @@ -52,8 +53,8 @@ <button on:click={decrement} class="reset pagination__item {canDecrement ? '' : 'c-disabled'}"> <svg class="icon icon--xs flip-x" - viewBox="0 0 16 16" - ><title>Go to previous page</title> + viewBox="0 0 16 16"> + <title>{$LL.views.dataTablePaginator.goToPrevPage()}</title> <polyline points="6 2 12 8 6 14" fill="none" @@ -68,26 +69,23 @@ <li> <span class="pagination__jumper flex items-center"> - <input - aria-label="Page number" - class="form-control" - id="curr-page" - type="text" - on:change={handle_change} - value={page} + <input aria-label="Page number" + class="form-control" + id="curr-page" + type="text" + on:change={handle_change} + value={page} /> - <em>of {pageCount}</em> + <em>{$LL.views.dataTablePaginator.of()} {pageCount}</em> </span> </li> <li> - <button - on:click={increment} - class="reset pagination__item {canIncrement ? '' : 'c-disabled'}" - > + <button on:click={increment} + class="reset pagination__item {canIncrement ? '' : 'c-disabled'}"> <svg class="icon icon--xs" - viewBox="0 0 16 16" - ><title>Go to next page</title> + viewBox="0 0 16 16"> + <title>{$LL.views.dataTablePaginator.goToNextPage()}</title> <polyline points="6 2 12 8 6 14" fill="none" diff --git a/apps/projects/src/app/pages/views/entry-form/index.svelte b/apps/projects/src/app/pages/views/entry-form/index.svelte index cb974ed..cf3d173 100644 --- a/apps/projects/src/app/pages/views/entry-form/index.svelte +++ b/apps/projects/src/app/pages/views/entry-form/index.svelte @@ -1,4 +1,5 @@ <script lang="ts"> + import LL from "$app/lib/i18n/i18n-svelte"; import {TimeEntryDto} from "$shared/lib/models/TimeEntryDto"; import {Temporal} from "@js-temporal/polyfill"; import {createEventDispatcher, onMount, onDestroy} from "svelte"; @@ -48,7 +49,7 @@ function description_is_valid() { if (!description) { - descriptionError = "Description is required"; + descriptionError = $LL.views.entryForm.errDescriptionReq(); } else { descriptionError = ""; } @@ -97,7 +98,7 @@ functions.reset(); dispatch("updated", response.data); } else { - formError = "An error occured while updating the entry, try again soon"; + formError = $LL.views.entryForm.entryUpdateError(); formIsLoading = false; } } else { @@ -106,7 +107,7 @@ functions.reset(); dispatch("created"); } else { - formError = "An error occured while creating the entry, try again soon"; + formError = $LL.views.entryForm.entryCreateError(); formIsLoading = false; } } @@ -175,14 +176,14 @@ <div class="margin-bottom-sm"> <Textarea class="width-100%" id="description" - label="Description" + label="{$LL.views.entryForm.description()}" errorText="{descriptionError}" bind:value={description}></Textarea> </div> <div class="flex flex-row justify-end gap-x-xs"> {#if entryId} - <Button text="Reset" + <Button text="{$LL.views.entryForm.reset()}" on:click={() => functions.reset()} variant="subtle" /> @@ -190,7 +191,7 @@ <Button loading={formIsLoading} type="submit" variant="primary" - text={entryId ? "Save" : "Create"} + text={entryId ? $LL.views.entryForm.save() : $LL.views.entryForm.create()} /> </div> </form> diff --git a/apps/projects/src/app/pages/views/entry-form/sections/category.svelte b/apps/projects/src/app/pages/views/entry-form/sections/category.svelte index aac84be..f7af382 100644 --- a/apps/projects/src/app/pages/views/entry-form/sections/category.svelte +++ b/apps/projects/src/app/pages/views/entry-form/sections/category.svelte @@ -3,6 +3,7 @@ import Dropdown from "$shared/components/dropdown.svelte"; import {is_guid, move_focus} from "$shared/lib/helpers"; import categories, {reload_categories, create_category_async} from "$app/lib/stores/categories"; + import LL from "$app/lib/i18n/i18n-svelte" let categoriesError = ""; let loading = false; @@ -12,7 +13,7 @@ function reset() { DropdownExports.reset(); categoriesError = ""; - console.log("Reset category-part"); + console.log($LL.views.entryForm.category._logReset()); } async function on_create({name}) { @@ -40,7 +41,7 @@ let isValid = true; const category = get_selected(); if (!is_guid(category?.id)) { - categoriesError = "Category is required"; + categoriesError = $LL.views.entryForm.category.errisRequired(); isValid = false; move_focus(document.getElementById("category-dropdown")); } else { @@ -60,15 +61,15 @@ <Dropdown entries={$categories} - label="Category" + label="{$LL.views.entryForm.category.category()}" maxlength="50" createable={true} - placeholder="Search or create" + placeholder="{$LL.views.entryForm.category.placeholder()}" id="category-dropdown" loading={loading} name="category-dropdown" on_create_async={on_create} - noResultsText="No categories available (Create a new one by searching for it)" + noResultsText="{$LL.views.entryForm.category.noResults()}" errorText="{categoriesError}" bind:this={DropdownExports} /> diff --git a/apps/projects/src/app/pages/views/entry-form/sections/date-time.svelte b/apps/projects/src/app/pages/views/entry-form/sections/date-time.svelte index c91e014..47b06e3 100644 --- a/apps/projects/src/app/pages/views/entry-form/sections/date-time.svelte +++ b/apps/projects/src/app/pages/views/entry-form/sections/date-time.svelte @@ -1,131 +1,134 @@ <script lang="ts"> - import {Temporal} from "@js-temporal/polyfill"; + import LL from "$app/lib/i18n/i18n-svelte"; + import {Temporal} from "@js-temporal/polyfill"; - // TIME - let fromTimeValue = ""; - let fromTimeError = ""; - let toTimeValue = ""; - let toTimeError = ""; + // TIME + let fromTimeValue = ""; + let fromTimeError = ""; + let toTimeValue = ""; + let toTimeError = ""; - function handle_from_time_changed(e) { - fromTimeValue = e.target.value; - if (fromTimeValue) { - fromTimeError = ""; - } - } + function handle_from_time_changed(e) { + fromTimeValue = e.target.value; + if (fromTimeValue) { + fromTimeError = ""; + } + } - function handle_to_time_changed(e) { - toTimeValue = e.target.value; - if (toTimeValue) { - toTimeError = ""; - } - } + function handle_to_time_changed(e) { + toTimeValue = e.target.value; + if (toTimeValue) { + toTimeError = ""; + } + } - // DATE - let date = Temporal.Now.plainDateTimeISO().toString().substring(0, 10); - let dateError = ""; + // DATE + let date = Temporal.Now.plainDateTimeISO().toString().substring(0, 10); + let dateError = ""; - function is_valid() { - let isValid = true; - let focusIsSet = false; - if (!date) { - dateError = "Date is required"; - isValid = false; - if (!focusIsSet) { - document.getElementById("date")?.focus(); - focusIsSet = true; - } - } else { - dateError = ""; - } + function is_valid() { + let isValid = true; + let focusIsSet = false; + if (!date) { + dateError = $LL.views.entryForm.dateTime.errDateIsRequired(); + isValid = false; + if (!focusIsSet) { + document.getElementById("date")?.focus(); + focusIsSet = true; + } + } else { + dateError = ""; + } - if (!fromTimeValue) { - fromTimeError = "From is required"; - isValid = false; - if (!focusIsSet) { - document.getElementById("from")?.focus(); - focusIsSet = true; - } - } else if (toTimeValue && fromTimeValue > toTimeValue) { - fromTimeError = "From can not be after To"; - isValid = false; - if (!focusIsSet) { - document.getElementById("from")?.focus(); - focusIsSet = true; - } - } else if (fromTimeValue === toTimeValue) { - fromTimeError = "From and To can not be equal"; - isValid = false; - if (!focusIsSet) { - document.getElementById("from")?.focus(); - focusIsSet = true; - } - } else { - fromTimeError = ""; - } + if (!fromTimeValue) { + fromTimeError = $LL.views.entryForm.dateTime.errFromIsRequired(); + isValid = false; + if (!focusIsSet) { + document.getElementById("from")?.focus(); + focusIsSet = true; + } + } else if (toTimeValue && fromTimeValue > toTimeValue) { + fromTimeError = $LL.views.entryForm.dateTime.errFromAfterTo(); + isValid = false; + if (!focusIsSet) { + document.getElementById("from")?.focus(); + focusIsSet = true; + } + } else if (fromTimeValue === toTimeValue) { + fromTimeError = $LL.views.entryForm.dateTime.errFromEqTo(); - if (!toTimeValue) { - toTimeError = "To is required"; - isValid = false; - if (!focusIsSet) { - document.getElementById("to")?.focus(); - focusIsSet = true; - } - } else if (fromTimeValue && toTimeValue < fromTimeValue) { - toTimeError = "To can not be before From"; - isValid = false; - if (!focusIsSet) { - document.getElementById("to")?.focus(); - focusIsSet = true; - } - } else { - toTimeError = ""; - } + isValid = false; + if (!focusIsSet) { + document.getElementById("from")?.focus(); + focusIsSet = true; + } + } else { + fromTimeError = ""; + } - return isValid; - } + if (!toTimeValue) { + toTimeError = $LL.views.entryForm.dateTime.errToIsRequired(); + isValid = false; + if (!focusIsSet) { + document.getElementById("to")?.focus(); + focusIsSet = true; + } + } else if (fromTimeValue && toTimeValue < fromTimeValue) { + toTimeError = $LL.views.entryForm.dateTime.errToBeforeFrom(); + isValid = false; + if (!focusIsSet) { + document.getElementById("to")?.focus(); + focusIsSet = true; + } + } else { + toTimeError = ""; + } - export const functions = { - get_from_time_value() { - return fromTimeValue; - }, - get_to_time_value() { - return toTimeValue; - }, - get_date() { - return date; - }, - is_valid, - reset(focusDate = false) { - fromTimeValue = ""; - toTimeValue = ""; - if (focusDate) { - document.getElementById("date")?.focus(); - } - }, - set_times(value) { - console.log(value); - fromTimeValue = value.from.toString().substring(0, 5); - toTimeValue = value.to.toString().substring(0, 5); - }, - set_date(new_date: Temporal.PlainDate) { - date = new_date.toString(); - }, - set_values(values) { - const currentTimeZone = Temporal.Now.timeZone().id; - const startDate = Temporal.Instant.from(values.start); - const stopDate = Temporal.Instant.from(values.stop); - fromTimeValue = startDate.toZonedDateTimeISO(currentTimeZone).toPlainTime().toString().substring(0, 5); - toTimeValue = stopDate.toZonedDateTimeISO(currentTimeZone).toPlainTime().toString().substring(0, 5); - date = startDate.toZonedDateTimeISO(currentTimeZone).toPlainDate().toString(); - } - }; + return isValid; + } + + export const functions = { + get_from_time_value() { + return fromTimeValue; + }, + get_to_time_value() { + return toTimeValue; + }, + get_date() { + return date; + }, + is_valid, + reset(focusDate = false) { + fromTimeValue = ""; + toTimeValue = ""; + if (focusDate) { + document.getElementById("date")?.focus(); + } + console.log($LL.views.entryForm.dateTime._logReset()); + }, + set_times(value) { + console.log(value); + fromTimeValue = value.from.toString().substring(0, 5); + toTimeValue = value.to.toString().substring(0, 5); + }, + set_date(new_date: Temporal.PlainDate) { + date = new_date.toString(); + }, + set_values(values) { + const currentTimeZone = Temporal.Now.timeZone().id; + const startDate = Temporal.Instant.from(values.start); + const stopDate = Temporal.Instant.from(values.stop); + fromTimeValue = startDate.toZonedDateTimeISO(currentTimeZone).toPlainTime().toString().substring(0, 5); + toTimeValue = stopDate.toZonedDateTimeISO(currentTimeZone).toPlainTime().toString().substring(0, 5); + date = startDate.toZonedDateTimeISO(currentTimeZone).toPlainDate().toString(); + } + }; </script> <div class="grid gap-xs"> <div class="col-4"> <label for="date" - class="form-label margin-bottom-xxs">Date</label> + class="form-label margin-bottom-xxs">{$LL.views.entryForm.dateTime.date()}</label> <input type="date" id="date" class="form-control width-100%" @@ -136,7 +139,7 @@ </div> <div class="col-4"> <label for="from" - class="form-label margin-bottom-xxs">From</label> + class="form-label margin-bottom-xxs">{$LL.views.entryForm.dateTime.from()}</label> <input id="from" class="form-control width-100%" pattern="[0-9][0-9]:[0-9][0-9]" @@ -150,7 +153,7 @@ </div> <div class="col-4"> <label for="to" - class="form-label margin-bottom-xxs">To</label> + class="form-label margin-bottom-xxs">{$LL.views.entryForm.dateTime.to()}</label> <input id="to" class="form-control width-100%" pattern="[0-9][0-9]:[0-9][0-9]" diff --git a/apps/projects/src/app/pages/views/entry-form/sections/labels.svelte b/apps/projects/src/app/pages/views/entry-form/sections/labels.svelte index f0853cc..a6f324b 100644 --- a/apps/projects/src/app/pages/views/entry-form/sections/labels.svelte +++ b/apps/projects/src/app/pages/views/entry-form/sections/labels.svelte @@ -1,4 +1,5 @@ <script> + import LL from "$app/lib/i18n/i18n-svelte"; import {generate_random_hex_color} from "$shared/lib/colors"; import labels, {reload_labels, create_label_async} from "$app/lib/stores/labels"; import Dropdown from "$shared/components/dropdown.svelte"; @@ -9,7 +10,7 @@ function reset() { DropdownExports.reset(); - console.log("Reset labels-part"); + console.log($LL.views.entryForm.labels._logReset()); } function get_selected() { @@ -50,15 +51,15 @@ <Dropdown entries={$labels} - label="Labels" + label="{$LL.views.entryForm.labels.labels()}" maxlength="50" createable={true} - placeholder="Search or create" + placeholder="{$LL.views.entryForm.labels.placeholder()}" multiple="{true}" id="labels-search" name="labels-search" on_create_async={on_create} - noResultsText="No labels available (Create a new one by searching for it)" + noResultsText="{$LL.views.entryForm.labels.placeholder()}" errorText="{labelsError}" bind:this={DropdownExports} {loading} diff --git a/apps/projects/src/app/pages/views/settings-categories-tile.svelte b/apps/projects/src/app/pages/views/settings-categories-tile.svelte index 890609a..8d2480f 100644 --- a/apps/projects/src/app/pages/views/settings-categories-tile.svelte +++ b/apps/projects/src/app/pages/views/settings-categories-tile.svelte @@ -8,6 +8,7 @@ import Button from "$shared/components/button.svelte"; import Tile from "$shared/components/tile.svelte"; import {Table, THead, TBody, TCell, TRow} from "$shared/components/table"; + import LL from "$app/lib/i18n/i18n-svelte"; let is_loading = true; let categories = []; @@ -38,9 +39,7 @@ if ( row && row.dataset.id && - confirm( - "Are you sure you want to delete this category?\nThis will delete all relating entries!" - ) + confirm($LL.views.settingsCategoriesTile.deleteAllConfirm()) ) { const response = await delete_time_category(row.dataset.id); if (response.ok) { @@ -56,14 +55,14 @@ </script> <Tile class="col-6@md col-12 {is_loading ? 'c-disabled loading' : ''}"> - <h2 class="margin-bottom-xxs">Categories</h2> + <h2 class="margin-bottom-xxs">{$LL.views.settingsCategoriesTile.categories()}</h2> {#if active_categories.length > 0 && archived_categories.length > 0} <nav class="s-tabs text-sm"> <ul class="s-tabs__list"> <li><a class="s-tabs__link s-tabs__link--current" - href="#0">Active ({active_categories.length})</a></li> + href="#0">{$LL.views.settingsCategoriesTile.active()} ({active_categories.length})</a></li> <li><a class="s-tabs__link" - href="#0">Archived ({archived_categories.length})</a></li> + href="#0">{$LL.views.settingsCategoriesTile.archived()} ({archived_categories.length})</a></li> </ul> </nav> {/if} @@ -72,11 +71,11 @@ <THead class="text-left"> <TCell type="th" thScope="col"> - Name + {$LL.views.settingsCategoriesTile.name()} </TCell> <TCell type="th" thScope="col"> - Color + {$LL.views.settingsCategoriesTile.color()} </TCell> <TCell type="th" thScope="col" @@ -102,13 +101,13 @@ class="hide" icon_height="1.2rem" on:click={handle_edit_category_click} - title="Edit entry"/> + title="{$LL.views.settingsCategoriesTile.editEntry()}"/> <Button icon="{IconNames.trash}" variant="reset" icon_width="1.2rem" icon_height="1.2rem" on:click={handle_delete_category_click} - title="Delete entry"/> + title="{$LL.views.settingsCategoriesTile.deleteEntry()}"/> </TCell> </TRow> @@ -117,7 +116,7 @@ <TRow> <TCell type="th" thScope="3"> - No categories + {$LL.views.settingsCategoriesTile.noCategories()} </TCell> </TRow> {/if} diff --git a/apps/projects/src/app/pages/views/settings-labels-tile.svelte b/apps/projects/src/app/pages/views/settings-labels-tile.svelte index f59e233..59b5e30 100644 --- a/apps/projects/src/app/pages/views/settings-labels-tile.svelte +++ b/apps/projects/src/app/pages/views/settings-labels-tile.svelte @@ -5,6 +5,7 @@ import Button from "$shared/components/button.svelte"; import Tile from "$shared/components/tile.svelte"; import {Table, THead, TBody, TCell, TRow} from "$shared/components/table"; + import LL from "$app/lib/i18n/i18n-svelte"; let is_loading = true; @@ -25,9 +26,7 @@ if ( row && row.dataset.id && - confirm( - "Are you sure you want to delete this label?\nIt will be removed from all related entries!" - ) + confirm($LL.views.settingsLabelsTile.deleteAllConfirm()) ) { await delete_label_async({id: row.dataset.id}); row.classList.add("d-none"); @@ -40,14 +39,14 @@ </script> <Tile class="col-6@md col-12 {is_loading ? 'c-disabled loading' : ''}"> - <h2 class="margin-bottom-xxs">Labels</h2> + <h2 class="margin-bottom-xxs">{$LL.views.settingsLabelsTile.labels()}</h2> {#if active_labels.length > 0 && archived_labels.length > 0} <nav class="s-tabs text-sm"> <ul class="s-tabs__list"> <li><a class="s-tabs__link s-tabs__link--current" - href="#0">Active ({active_labels.length})</a></li> + href="#0">{$LL.views.settingsLabelsTile.active()} ({active_labels.length})</a></li> <li><a class="s-tabs__link" - href="#0">Archived ({archived_labels.length})</a></li> + href="#0">{$LL.views.settingsLabelsTile.archived()} ({archived_labels.length})</a></li> </ul> </nav> {/if} @@ -56,11 +55,11 @@ <THead class="text-left"> <TCell type="th" thScope="row"> - Name + {$LL.views.settingsLabelsTile.name()} </TCell> <TCell type="th" thScope="row"> - Color + {$LL.views.settingsLabelsTile.color()} </TCell> <TCell type="th" thScope="row" @@ -87,13 +86,13 @@ class="hide" icon_height="1.2rem" on:click={handle_edit_label_click} - title="Edit entry"/> + title="{$LL.views.settingsLabelsTile.editEntry()}"/> <Button icon="{IconNames.trash}" variant="reset" icon_width="1.2rem" icon_height="1.2rem" on:click={handle_delete_label_click} - title="Delete entry"/> + title="{$LL.views.settingsLabelsTile.deleteEntry()}"/> </TCell> </TRow> {/each} |
