diff options
| author | Fuwn <[email protected]> | 2026-05-24 13:22:34 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-05-24 13:22:34 +0000 |
| commit | 56a7a7851b09cb30a5cd543c8cb4f926109b4290 (patch) | |
| tree | a620f908405fa48fd601580c5a48432831ec5c33 /src/lib | |
| parent | fix(layout): preserve list panel when clicking action buttons in summary (diff) | |
| download | due.moe-56a7a7851b09cb30a5cd543c8cb4f926109b4290.tar.xz due.moe-56a7a7851b09cb30a5cd543c8cb4f926109b4290.zip | |
refactor(locale): move hardcoded UI strings into english locale
Adds optional namespaces (common, errors, commandPalette, headTitle,
notifications, schedule, events, home, reader, routes, badgePreview,
badgeWall) and extends existing ones (settings.*, lists.*, tools.*,
user.*, hololive.*) on the Locale interface. New fields are optional
so japanese.ts can omit them; svelte-i18n's fallbackLocale handles
the runtime miss.
HeadTitle gains an optional routeKey prop for type-safe lookup.
defaultActions becomes a factory so the command palette re-reads
locale on language toggle. The existing JP feedback translation
in routes/settings is preserved via japanese.ts.
Out of scope (kept hardcoded): service-worker.ts, app.html,
Landing*.svelte, tools.ts registry, Easter Event 2025 pages.
Diffstat (limited to 'src/lib')
50 files changed, 1597 insertions, 477 deletions
diff --git a/src/lib/CommandPalette/CommandPalette.svelte b/src/lib/CommandPalette/CommandPalette.svelte index b68d468d..04eabff9 100644 --- a/src/lib/CommandPalette/CommandPalette.svelte +++ b/src/lib/CommandPalette/CommandPalette.svelte @@ -3,6 +3,7 @@ import { onMount } from "svelte"; import { fly, fade } from "svelte/transition"; import { flip } from "svelte/animate"; import type { CommandPaletteAction } from "./actions"; +import locale from "$stores/locale"; export let items: CommandPaletteAction[] = []; export let open = false; @@ -176,7 +177,7 @@ const handleGlobalKey = (e: KeyboardEvent) => { class="dropdown {open ? 'fade-in' : 'fade-out'}" role="dialog" aria-modal="true" - aria-label="Command palette" + aria-label={$locale().commandPalette?.ariaLabel} > <div class="dropdown-content card card-small card-glass"> <input @@ -184,8 +185,8 @@ const handleGlobalKey = (e: KeyboardEvent) => { bind:value={search} class="command-input" type="text" - placeholder="Search" - aria-label="Search commands" + placeholder={$locale().commandPalette?.placeholder} + aria-label={$locale().commandPalette?.searchCommands} autocomplete="off" spellcheck="false" role="combobox" @@ -196,7 +197,12 @@ const handleGlobalKey = (e: KeyboardEvent) => { onkeydown={handleKey} /> - <div class="results-container" role="listbox" id={listboxId} aria-label="Commands"> + <div + class="results-container" + role="listbox" + id={listboxId} + aria-label={$locale().commandPalette?.commands} + > {#each filtered as item, i (item.id || item.url)} <a id={optionDomId(i)} @@ -220,7 +226,7 @@ const handleGlobalKey = (e: KeyboardEvent) => { {#if filtered.length === 0 && search !== ''} <div class="no-results opaque" role="status" in:fade={{ duration: 150 }}> - No results found + {$locale().commandPalette?.noResults} </div> {/if} </div> diff --git a/src/lib/CommandPalette/actions.ts b/src/lib/CommandPalette/actions.ts index 9915fe66..d2ed5a1d 100644 --- a/src/lib/CommandPalette/actions.ts +++ b/src/lib/CommandPalette/actions.ts @@ -1,4 +1,6 @@ import { invalidateListCaches } from "$lib/Media/invalidate"; +import locale from "$stores/locale"; +import { get } from "svelte/store"; export interface CommandPaletteAction { name: string; @@ -9,178 +11,182 @@ export interface CommandPaletteAction { actions?: CommandPaletteAction[]; } -export const defaultActions: CommandPaletteAction[] = [ - { - name: "Home", - url: "/", - tags: [ - "main", - "manga", - "anime", - "light", - "dashboard", - "start", - "begin", - "novels", - "list", - ], - actions: [ - { - name: "Upcoming Episodes", - url: "/", - tags: ["anime", "list"], - }, - { - name: "Not Yet Released", - url: "/", - tags: ["anime", "schedule", "list"], - }, - { - name: "Due Episodes", - url: "/", - tags: ["anime", "list"], - }, - { - name: "Manga & Light Novels", - url: "/", - tags: ["novels", "manga", "list"], - }, - ], - }, - { - name: "Completed", - url: "/completed", - tags: [ - "finish", - "end", - "done", - "finish", - "end", - "done", - "anime", - "novels", - "manga", - ], - actions: [ - { - name: "Anime", - url: "/completed", - tags: ["anime", "list"], - }, - { - name: "Manga & Light Novels", - url: "/completed", - tags: ["novels", "manga", "list"], - }, - ], - }, - { - name: "Subtitle Schedule", - url: "/schedule", - tags: ["anime", "subs"], - }, - { - name: "hololive Schedule", - url: "/hololive", - tags: ["vtuber", "youtube", "virtual", "twitch", "stream"], - }, - { - name: "Character Birthdays", - url: "/birthdays", - tags: ["schedule", "vtuber", "date"], - }, - { - name: "New Releases", - url: "/releases", - tags: ["novels", "manga", "date", "schedule", "time"], - }, - { - name: "Settings", - url: "/settings", - tags: [ - "sync", - "display", - "hide", - "panels", - "motion", - "accessibility", - "notifications", - "rss", - "warning", - "show", - "links", - "sort", - "calculation", - "cache", - "clear", - "debug", - "language", - "locale", - ], - actions: [ - { - name: "Settings Sync", - url: "/settings#sync", - tags: ["settings"], - }, - { - name: "RSS Feeds", - url: "/settings#feeds", - tags: ["settings"], - }, - { - name: "Display", - url: "/settings", - tags: ["settings"], - }, - { - name: "Calculation", - url: "/settings", - tags: ["settings"], - }, - { - name: "Cache", - url: "/settings", - tags: ["settings"], - }, - { - name: "Debug", - url: "/settings#debug", - tags: ["settings"], - }, - ], - }, - { - name: "My Profile", - url: "/user", - tags: ["user", "me", "settings"], - actions: [ - { - name: "User Preferences", - url: "/user", - tags: ["user", "me", "settings"], - }, - ], - }, - { - name: "My Badge Wall", - url: "/user?badges=1", - tags: ["user", "me", "settings"], - }, - { - name: "Refresh Anime & Manga List Caches", - url: "", - preventDefault: true, - tags: [ - "cache", - "clear", - "refresh", - "invalidate", - "debug", - "anime", - "manga", - "list", - ], - onClick: invalidateListCaches, - }, -]; +export const defaultActions = (): CommandPaletteAction[] => { + const l = get(locale)(); + + return [ + { + name: l.navigation.home, + url: "/", + tags: [ + "main", + "manga", + "anime", + "light", + "dashboard", + "start", + "begin", + "novels", + "list", + ], + actions: [ + { + name: l.lists.upcoming.episodes.title, + url: "/", + tags: ["anime", "list"], + }, + { + name: l.lists.upcoming.notYetReleased.title, + url: "/", + tags: ["anime", "schedule", "list"], + }, + { + name: l.lists.due.episodes.title, + url: "/", + tags: ["anime", "list"], + }, + { + name: l.lists.due.mangaAndLightNovels.title, + url: "/", + tags: ["novels", "manga", "list"], + }, + ], + }, + { + name: l.navigation.completed, + url: "/completed", + tags: [ + "finish", + "end", + "done", + "finish", + "end", + "done", + "anime", + "novels", + "manga", + ], + actions: [ + { + name: l.settings.media.anime, + url: "/completed", + tags: ["anime", "list"], + }, + { + name: l.lists.completed.mangaAndLightNovels.title, + url: "/completed", + tags: ["novels", "manga", "list"], + }, + ], + }, + { + name: l.navigation.subtitleSchedule, + url: "/schedule", + tags: ["anime", "subs"], + }, + { + name: l.navigation.hololive, + url: "/hololive", + tags: ["vtuber", "youtube", "virtual", "twitch", "stream"], + }, + { + name: l.tools.tool.characterBirthdays.short, + url: "/birthdays", + tags: ["schedule", "vtuber", "date"], + }, + { + name: l.navigation.newReleases, + url: "/releases", + tags: ["novels", "manga", "date", "schedule", "time"], + }, + { + name: l.navigation.settings, + url: "/settings", + tags: [ + "sync", + "display", + "hide", + "panels", + "motion", + "accessibility", + "notifications", + "rss", + "warning", + "show", + "links", + "sort", + "calculation", + "cache", + "clear", + "debug", + "language", + "locale", + ], + actions: [ + { + name: l.settings.settingsSync.title, + url: "/settings#sync", + tags: ["settings"], + }, + { + name: l.settings.rssFeeds.title, + url: "/settings#feeds", + tags: ["settings"], + }, + { + name: l.settings.display.title, + url: "/settings", + tags: ["settings"], + }, + { + name: l.settings.calculation.title, + url: "/settings", + tags: ["settings"], + }, + { + name: l.settings.cache.title, + url: "/settings", + tags: ["settings"], + }, + { + name: l.settings.debug.title, + url: "/settings#debug", + tags: ["settings"], + }, + ], + }, + { + name: l.navigation.myProfile, + url: "/user", + tags: ["user", "me", "settings"], + actions: [ + { + name: l.user.preferences.title, + url: "/user", + tags: ["user", "me", "settings"], + }, + ], + }, + { + name: l.navigation.myBadgeWall, + url: "/user?badges=1", + tags: ["user", "me", "settings"], + }, + { + name: l.commandPalette?.refreshCaches ?? "Refresh Anime & Manga List Caches", + url: "", + preventDefault: true, + tags: [ + "cache", + "clear", + "refresh", + "invalidate", + "debug", + "anime", + "manga", + "list", + ], + onClick: invalidateListCaches, + }, + ]; +}; diff --git a/src/lib/CommandPalette/authActions.ts b/src/lib/CommandPalette/authActions.ts index c306eb34..9dbc9565 100644 --- a/src/lib/CommandPalette/authActions.ts +++ b/src/lib/CommandPalette/authActions.ts @@ -1,15 +1,19 @@ import { env } from "$env/dynamic/public"; import root from "$lib/Utility/root"; import localforage from "localforage"; +import locale from "$stores/locale"; +import { get } from "svelte/store"; import type { CommandPaletteAction } from "./actions"; export const authActions = ( user: string | undefined, ): CommandPaletteAction[] => { + const l = get(locale)(); + if (user) return [ { - name: "Log Out", + name: l.commandPalette?.logOut ?? "Log Out", url: "#", preventDefault: true, tags: ["auth", "sign", "out", "user"], @@ -29,7 +33,7 @@ export const authActions = ( return [ { - name: "Log In", + name: l.commandPalette?.logIn ?? "Log In", url: loginUrl, preventDefault: true, tags: ["auth", "sign", "in", "anilist"], diff --git a/src/lib/CommandPalette/syncActions.ts b/src/lib/CommandPalette/syncActions.ts index 90c6a931..b3cf01c6 100644 --- a/src/lib/CommandPalette/syncActions.ts +++ b/src/lib/CommandPalette/syncActions.ts @@ -4,6 +4,7 @@ import root from "$lib/Utility/root"; import settings from "$stores/settings"; import settingsSyncPulled from "$stores/settingsSyncPulled"; import settingsSyncTimes from "$stores/settingsSyncTimes"; +import locale from "$stores/locale"; import { get } from "svelte/store"; import type { CommandPaletteAction } from "./actions"; @@ -13,9 +14,11 @@ export const syncActions = ( ): CommandPaletteAction[] => { if (identityId <= 0) return []; + const l = get(locale)(); + const actions: CommandPaletteAction[] = [ { - name: "Push Settings Now", + name: l.commandPalette?.sync?.pushNow ?? "Push Settings Now", url: "#", preventDefault: true, tags: ["settings", "sync", "push", "upload", "remote"], @@ -32,8 +35,10 @@ export const syncActions = ( addNotification( options({ - heading: "Settings Sync", - description: "Pushed local configuration to remote", + heading: get(locale)().settings.settingsSync.title, + description: + get(locale)().commandPalette?.sync?.pushedDescription ?? + "Pushed local configuration to remote", }), ); @@ -46,7 +51,7 @@ export const syncActions = ( }, }, { - name: "Pull Settings Now", + name: l.commandPalette?.sync?.pullNow ?? "Pull Settings Now", url: "#", preventDefault: true, tags: ["settings", "sync", "pull", "download", "remote"], @@ -61,8 +66,10 @@ export const syncActions = ( if (!data?.configuration) { addNotification( options({ - heading: "Settings Sync", - description: "No remote configuration found", + heading: get(locale)().settings.settingsSync.title, + description: + get(locale)().commandPalette?.sync?.noRemoteFound ?? + "No remote configuration found", }), ); @@ -77,7 +84,11 @@ export const syncActions = ( }); addNotification( - options({ heading: "Pulled remote configuration" }), + options({ + heading: + get(locale)().notifications?.pulledRemote ?? + "Pulled remote configuration", + }), ); }); }) @@ -88,14 +99,20 @@ export const syncActions = ( if (syncEnabled) actions.push({ - name: "Disable Settings Sync", + name: l.commandPalette?.sync?.disable ?? "Disable Settings Sync", url: "#", preventDefault: true, tags: ["settings", "sync", "disable", "off", "stop"], onClick: () => { settings.setKey("settingsSync", false); - addNotification(options({ heading: "Settings sync disabled" })); + addNotification( + options({ + heading: + get(locale)().notifications?.syncDisabled ?? + "Settings sync disabled", + }), + ); }, }); diff --git a/src/lib/CommandPalette/toggleActions.ts b/src/lib/CommandPalette/toggleActions.ts index ca64ff5f..9ba1d121 100644 --- a/src/lib/CommandPalette/toggleActions.ts +++ b/src/lib/CommandPalette/toggleActions.ts @@ -1,4 +1,6 @@ import settings, { type Settings } from "$stores/settings"; +import locale from "$stores/locale"; +import { get } from "svelte/store"; import type { CommandPaletteAction } from "./actions"; const TITLE_FORMATS: Settings["displayTitleFormat"][] = [ @@ -51,82 +53,84 @@ export const toggleActions = (current: Settings): CommandPaletteAction[] => { const nextOutbound = OUTBOUND_TARGETS[(outboundIndex + 1) % OUTBOUND_TARGETS.length]; + const t = get(locale)().commandPalette?.toggles; + return [ boolToggle( current.display24HourTime, "display24HourTime", - "Switch to 24-hour time", - "Switch to 12-hour time", + t?.time24On ?? "Switch to 24-hour time", + t?.time24Off ?? "Switch to 12-hour time", ["time", "clock", "24h", "12h", "format"], ), boolToggle( current.displayDisableAnimations, "displayDisableAnimations", - "Disable animations", - "Enable animations", + t?.animationsOff ?? "Disable animations", + t?.animationsOn ?? "Enable animations", ["motion", "animation", "accessibility"], ), boolToggle( current.displayBlurAdultContent, "displayBlurAdultContent", - "Blur adult content", - "Show adult content unblurred", + t?.blurAdultOn ?? "Blur adult content", + t?.blurAdultOff ?? "Show adult content unblurred", ["nsfw", "adult", "blur", "censor"], ), boolToggle( current.displayCoverModeAnime, "displayCoverModeAnime", - "Show anime covers", - "Hide anime covers", + t?.showAnimeCovers ?? "Show anime covers", + t?.hideAnimeCovers ?? "Hide anime covers", ["cover", "image", "anime", "display"], ), boolToggle( current.displayCoverModeManga, "displayCoverModeManga", - "Show manga covers", - "Hide manga covers", + t?.showMangaCovers ?? "Show manga covers", + t?.hideMangaCovers ?? "Hide manga covers", ["cover", "image", "manga", "novels", "display"], ), boolToggle( current.displayHoverCover, "displayHoverCover", - "Enable hover cover preview", - "Disable hover cover preview", + t?.hoverCoverOn ?? "Enable hover cover preview", + t?.hoverCoverOff ?? "Disable hover cover preview", ["cover", "hover", "preview"], ), boolToggle( current.displayScheduleListMode, "displayScheduleListMode", - "Schedule: list mode", - "Schedule: grid mode", + t?.scheduleListMode ?? "Schedule: list mode", + t?.scheduleGridMode ?? "Schedule: grid mode", ["schedule", "list", "grid", "view"], ), boolToggle( current.displayReverseSort, "displayReverseSort", - "Reverse sort order", - "Restore default sort order", + t?.reverseSort ?? "Reverse sort order", + t?.restoreSort ?? "Restore default sort order", ["sort", "reverse", "order"], ), boolToggle( current.displayDataSaver, "displayDataSaver", - "Enable data saver", - "Disable data saver", + t?.dataSaverOn ?? "Enable data saver", + t?.dataSaverOff ?? "Disable data saver", ["data", "bandwidth", "saver", "performance"], ), boolToggle( current.displayDisableNotifications, "displayDisableNotifications", - "Disable in-app notifications", - "Enable in-app notifications", + t?.notificationsOff ?? "Disable in-app notifications", + t?.notificationsOn ?? "Enable in-app notifications", ["notifications", "alerts", "toast"], ), { name: current.displayLanguage === "en" - ? "Switch language to 日本語" - : "Switch language to English", + ? (t?.switchLanguageJa ?? "Switch language to 日本語") + : (t?.switchLanguageEn ?? "Switch language to English"), url: "#", preventDefault: true, tags: ["language", "locale", "translate", "japanese", "english"], @@ -137,14 +141,18 @@ export const toggleActions = (current: Settings): CommandPaletteAction[] => { ), }, { - name: `Title format: ${current.displayTitleFormat} → ${nextTitle}`, + name: (t?.titleFormat ?? "Title format: {from} → {to}") + .replace("{from}", current.displayTitleFormat) + .replace("{to}", nextTitle), url: "#", preventDefault: true, tags: ["title", "format", "english", "romaji", "native"], onClick: () => settings.setKey("displayTitleFormat", nextTitle), }, { - name: `Outbound links: ${OUTBOUND_LABELS[current.displayOutboundLinksTo]} → ${OUTBOUND_LABELS[nextOutbound]}`, + name: (t?.outboundLinks ?? "Outbound links: {from} → {to}") + .replace("{from}", OUTBOUND_LABELS[current.displayOutboundLinksTo]) + .replace("{to}", OUTBOUND_LABELS[nextOutbound]), url: "#", preventDefault: true, tags: [ diff --git a/src/lib/Data/AniList/media.ts b/src/lib/Data/AniList/media.ts index b9e0ef3f..0a83dda6 100644 --- a/src/lib/Data/AniList/media.ts +++ b/src/lib/Data/AniList/media.ts @@ -9,6 +9,8 @@ import lastPruneTimes from "$stores/lastPruneTimes"; import { options as getOptions, type Options } from "$lib/Notification/options"; import type { PrequelRelation, PrequelRelations } from "./prequels"; import localforage from "localforage"; +import locale from "$stores/locale"; +import { get } from "svelte/store"; export enum Type { Anime, @@ -412,7 +414,9 @@ export const mediaListCollection = async ( heading: options.notificationType ? options.notificationType : Type[type], - description: "Re-cached media lists from AniList", + description: + get(locale)().notifications?.recachedFromAniList ?? + "Re-cached media lists from AniList", }), ); diff --git a/src/lib/Error/AnimeRateLimited.svelte b/src/lib/Error/AnimeRateLimited.svelte index 70509a0c..c8b822e0 100644 --- a/src/lib/Error/AnimeRateLimited.svelte +++ b/src/lib/Error/AnimeRateLimited.svelte @@ -1,12 +1,13 @@ <script> import Spacer from "$lib/Layout/Spacer.svelte"; import Popup from "$lib/Layout/Popup.svelte"; +import locale from "$stores/locale"; </script> <Popup locked fullscreen> <p><slot /></p> - <span>It is likely that you have been rate-limited by AniList. Please try again later.</span> + <span>{$locale().errors?.animeRateLimited}</span> {#await fetch('https://api.waifu.pics/sfw/cry') then response} {#await response.json() then json} diff --git a/src/lib/Error/LogInRestricted.svelte b/src/lib/Error/LogInRestricted.svelte index 999f2db3..0d4f00e3 100644 --- a/src/lib/Error/LogInRestricted.svelte +++ b/src/lib/Error/LogInRestricted.svelte @@ -2,18 +2,19 @@ import Popup from "$lib/Layout/Popup.svelte"; import { env } from "$env/dynamic/public"; import localforage from "localforage"; +import locale from "$stores/locale"; </script> <Popup fullscreen locked> <div class="message"> - Please <a + {$locale().errors?.loginRequiredPrefix}<a href={`https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code`} onclick={async () => { await localforage.setItem( 'redirect', window.location.origin + window.location.pathname + window.location.search ); - }}>log in</a - > to view this page. + }}>{$locale().errors?.loginRequiredLink}</a + >{$locale().errors?.loginRequiredSuffix} </div> </Popup> diff --git a/src/lib/Error/RateLimited.svelte b/src/lib/Error/RateLimited.svelte index 2a79efb6..890c4d20 100644 --- a/src/lib/Error/RateLimited.svelte +++ b/src/lib/Error/RateLimited.svelte @@ -1,5 +1,6 @@ <script lang="ts"> import Spacer from "$lib/Layout/Spacer.svelte"; +import locale from "$stores/locale"; export let type = "Media"; export let loginSessionError = true; export let contact = true; @@ -13,15 +14,18 @@ export let might = true; <ul> <li> <p> - {type} could not be loaded. You{might ? ' might' : ''} have been rate-limited. {#if !might} - Try again in one minute. + {type} + {might + ? $locale().errors?.rateLimited?.notLoadedMight + : $locale().errors?.rateLimited?.notLoadedDefinitely} + {#if !might} + {$locale().errors?.rateLimited?.tryAgainOneMinute} {/if} </p> {#if loginSessionError} <p> - Your login session may have expired. Try logging out and logging back in, or try again - in a few minutes. + {$locale().errors?.rateLimited?.sessionExpired} </p> {/if} @@ -30,22 +34,25 @@ export let might = true; {#if contact} <Spacer /> - If the problem persists, please contact - <a href="https://anilist.co/user/fuwn" target="_blank">@fuwn</a> on AniList. + {$locale().errors?.rateLimited?.contactSupport?.split('@fuwn')[0]} + <a href="https://anilist.co/user/fuwn" target="_blank">@fuwn</a>{$locale().errors?.rateLimited?.contactSupport?.split('@fuwn')[1]} {/if} </li> </ul> {:else} <p> - {type} could not be loaded. You{might ? ' might' : ''} have been rate-limited. {#if !might} - Try again in one minute. + {type} + {might + ? $locale().errors?.rateLimited?.notLoadedMight + : $locale().errors?.rateLimited?.notLoadedDefinitely} + {#if !might} + {$locale().errors?.rateLimited?.tryAgainOneMinute} {/if} </p> {#if loginSessionError} <p> - Your login session may have expired. Try logging out and logging back in, or try again in a - few minutes. + {$locale().errors?.rateLimited?.sessionExpired} </p> {/if} @@ -54,8 +61,8 @@ export let might = true; {#if contact} <Spacer /> - If the problem persists, please contact - <a href="https://anilist.co/user/fuwn" target="_blank">@fuwn</a> on AniList. + {$locale().errors?.rateLimited?.contactSupport?.split('@fuwn')[0]} + <a href="https://anilist.co/user/fuwn" target="_blank">@fuwn</a>{$locale().errors?.rateLimited?.contactSupport?.split('@fuwn')[1]} {/if} {/if} </div> diff --git a/src/lib/Hololive/Lives.svelte b/src/lib/Hololive/Lives.svelte index 1969fa4f..eb357104 100644 --- a/src/lib/Hololive/Lives.svelte +++ b/src/lib/Hololive/Lives.svelte @@ -4,6 +4,7 @@ import Message from "$lib/Loading/Message.svelte"; import root from "$lib/Utility/root"; import type { Live, ParseResult } from "./hololive"; import Stream from "./Stream.svelte"; +import locale from "$stores/locale"; export let schedule: ParseResult; export let pinnedStreams: string[]; @@ -60,7 +61,7 @@ $: categorisedStreams = schedule.lives </script> {#if schedule.lives.length === 0} - <Message message="No upcoming streams." loader="ripple" /> + <Message message={$locale().hololive.noUpcomingStreams} loader="ripple" /> {/if} <div class="container"> diff --git a/src/lib/Home/HeadTitle.svelte b/src/lib/Home/HeadTitle.svelte index 0759943f..73cf3e3d 100644 --- a/src/lib/Home/HeadTitle.svelte +++ b/src/lib/Home/HeadTitle.svelte @@ -1,9 +1,24 @@ <script lang="ts"> import { siteUrl } from "$lib/Utility/appOrigin"; +import locale from "$stores/locale"; +import type { Locale } from "$lib/Locale/layout"; -let { route = undefined, path = "/" }: { route?: string; path?: string } = - $props(); -const title = $derived((route ? `${route} • ` : "") + "due.moe"); +type HeadTitleKey = keyof NonNullable<Locale["headTitle"]>; + +let { + route = undefined, + routeKey = undefined, + path = "/", +}: { + route?: string; + routeKey?: HeadTitleKey; + path?: string; +} = $props(); + +const resolved = $derived( + routeKey ? ($locale().headTitle?.[routeKey] ?? routeKey) : route, +); +const title = $derived((resolved ? `${resolved} • ` : "") + "due.moe"); </script> <svelte:head> diff --git a/src/lib/Home/LastActivity.svelte b/src/lib/Home/LastActivity.svelte index 4ade371a..1b4952f8 100644 --- a/src/lib/Home/LastActivity.svelte +++ b/src/lib/Home/LastActivity.svelte @@ -4,6 +4,8 @@ import { onMount } from "svelte"; import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; import { lastActivityDate } from "../Data/AniList/lastActivity"; import settings from "$stores/settings"; +import locale from "$stores/locale"; +import { get } from "svelte/store"; let { user }: { user: AniListAuthorisation } = $props(); let lastActivityWasToday = $state(true); @@ -34,6 +36,7 @@ const timeLeftToday = () => { const hoursLeft = 24 - currentHour; let minutesLeft = 0; let timeLeft = ""; + const l = get(locale)(); if (hoursLeft > 0) { minutesLeft = hoursLeft * 60 - currentMinute; @@ -42,9 +45,9 @@ const timeLeftToday = () => { } if (minutesLeft > 60) { - timeLeft = `${Math.round(minutesLeft / 60)} hours`; + timeLeft = `${Math.round(minutesLeft / 60)} ${l.home?.lastActivity?.hoursUnit ?? "hours"}`; } else { - timeLeft = `${minutesLeft} minutes`; + timeLeft = `${minutesLeft} ${l.home?.lastActivity?.minutesUnit ?? "minutes"}`; } return timeLeft; @@ -53,7 +56,6 @@ const timeLeftToday = () => { {#if !$settings.displayDisableLastActivityWarning && !lastActivityWasToday} <blockquote> - You don't have any new activity statuses from the past day! Create one within {timeLeftToday()} - to keep your streak! + {$locale({ values: { timeLeft: timeLeftToday() } }).home?.lastActivity?.warning} </blockquote> {/if} diff --git a/src/lib/Image/FallbackImage.svelte b/src/lib/Image/FallbackImage.svelte index 8ff6f6c3..a92a32b1 100644 --- a/src/lib/Image/FallbackImage.svelte +++ b/src/lib/Image/FallbackImage.svelte @@ -1,4 +1,6 @@ <script lang="ts"> +import locale from "$stores/locale"; + export let source: string | undefined | null; export let alternative: string | undefined | null; export let fallback: string | undefined | null; @@ -32,7 +34,7 @@ const delayedReplace = (event: Event, image: string | undefined | null) => { {style} /> {:else if !hideOnError} - <img src={error} alt="Not found" loading="lazy" class="badge" /> + <img src={error} alt={$locale().common?.notFound ?? 'Not found'} loading="lazy" class="badge" /> {/if} <style> diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte index 42f4d933..5cb6cb1e 100644 --- a/src/lib/List/Anime/CleanAnimeList.svelte +++ b/src/lib/List/Anime/CleanAnimeList.svelte @@ -1,5 +1,6 @@ <script lang="ts"> import Spacer from "$lib/Layout/Spacer.svelte"; +import locale from "$stores/locale"; import settings from "$stores/settings"; import type { Media } from "$lib/Data/AniList/media"; @@ -250,7 +251,7 @@ const increment = (anime: Media, progress: number) => { <button class="small-button" onclick={() => (showRoulette = true)} - title="Pick a random anime to watch" + title={$locale().lists.actions?.pickRandomAnime} > Roulette </button> @@ -258,12 +259,13 @@ const increment = (anime: Media, progress: number) => { </ListTitle> {#if media.length === 0} - No anime to display. <button onclick={() => (animeLists = cleanCache(user, $identity))}> - Force refresh + {$locale().lists.empty?.anime} + <button onclick={() => (animeLists = cleanCache(user, $identity))}> + {$locale().lists.actions?.forceRefresh} </button> {:else if $settings.displayMediaListFilter && !disableFilter && hasDistinguishingList} <select value={selectedList} onchange={updateSelectedList}> - <option value="All">All</option> + <option value="All">{$locale().lists.actions?.all}</option> {#each lists as list} <option value={list}>{list}</option> diff --git a/src/lib/List/Manga/CleanMangaList.svelte b/src/lib/List/Manga/CleanMangaList.svelte index a52a9d7e..25e6d48f 100644 --- a/src/lib/List/Manga/CleanMangaList.svelte +++ b/src/lib/List/Manga/CleanMangaList.svelte @@ -145,7 +145,7 @@ const increment = (manga: Media) => { {#if !dummy} <button class="small-button" - title="Force a full refresh" + title={$locale().lists.actions?.forceFullRefresh} onclick={cleanCache} data-umami-event="Force Refresh Manga">Refresh</button > @@ -153,7 +153,7 @@ const increment = (manga: Media) => { <button class="small-button" onclick={() => (showRoulette = true)} - title="Pick a random manga to read" + title={$locale().lists.actions?.pickRandomManga} > Roulette </button> @@ -168,11 +168,13 @@ const increment = (manga: Media) => { {:then status} {#if status} {#if status.status === 503} - <a href="https://due.moe">due.moe</a>'s manga data source is currently down for maintenance. - Please check back later. + {$locale().lists.errors?.mangaDataDown?.split("due.moe's")[0]}<a href="https://due.moe" + >due.moe</a + >{$locale().lists.errors?.mangaDataDown?.split("due.moe")[1]} {:else if status.status !== 200} - <a href="https://due.moe">due.moe</a>'s manga data source is currently unavailable. Please - check back later. + {$locale().lists.errors?.mangaDataUnavailable?.split("due.moe's")[0]}<a + href="https://due.moe">due.moe</a + >{$locale().lists.errors?.mangaDataUnavailable?.split("due.moe")[1]} {:else} <RateLimitedError /> {/if} @@ -180,8 +182,9 @@ const increment = (manga: Media) => { <Skeleton card={false} count={1} height="0.9rem" list /> {/if} {:catch} - <a href="https://due.moe">due.moe</a>'s manga data source is currently unreachable. Please check - back later. + {$locale().lists.errors?.mangaDataUnreachable?.split("due.moe's")[0]}<a href="https://due.moe" + >due.moe</a + >{$locale().lists.errors?.mangaDataUnreachable?.split("due.moe")[1]} {/await} {/if} @@ -191,21 +194,24 @@ const increment = (manga: Media) => { {/if} <p> - No manga to display. <button onclick={cleanCache} data-umami-event="Force Refresh No Manga" - >Force refresh</button + {$locale().lists.empty?.manga} + <button onclick={cleanCache} data-umami-event="Force Refresh No Manga" + >{$locale().lists.actions?.forceRefresh}</button > </p> <span> - Don't read manga? <button - onclick={() => ($settings.disableManga = true)} - data-umami-event="Disable No Manga">Hide the manga panel</button + {$locale().lists.dontReadMangaPrefix} + <button onclick={() => ($settings.disableManga = true)} data-umami-event="Disable No Manga" + >{$locale().lists.hideMangaPanel}</button > - You can re-enable it later in the <a href={root('/settings')}>Settings</a>. + {$locale().lists.reenableInSettings?.split('Settings')[0]}<a href={root('/settings')} + >{$locale().navigation.settings}</a + >. </span> {:else if $settings.displayMediaListFilter && !disableFilter && hasDistinguishingList} <select value={selectedList} onchange={updateSelectedList}> - <option value="All">All</option> + <option value="All">{$locale().lists.actions?.all}</option> {#each lists as list} <option value={list}>{list}</option> diff --git a/src/lib/List/Manga/MangaListTemplate.svelte b/src/lib/List/Manga/MangaListTemplate.svelte index df894910..1bb53be1 100644 --- a/src/lib/List/Manga/MangaListTemplate.svelte +++ b/src/lib/List/Manga/MangaListTemplate.svelte @@ -18,6 +18,7 @@ import privilegedUser from "$lib/Utility/privilegedUser"; import identity from "$stores/identity"; import lastPruneTimes from "$stores/lastPruneTimes"; import locale from "$stores/locale"; +import { get } from "svelte/store"; import manga from "$stores/manga"; import revalidateManga from "$stores/revalidateManga"; import settings from "$stores/settings"; @@ -211,8 +212,10 @@ const cleanMedia = async ( if (refreshing) { addNotification( options({ - heading: "Manga", - description: "Re-freshing manga data ...", + heading: get(locale)().notifications?.mangaHeading ?? "Manga", + description: + get(locale)().notifications?.mangaRefreshing ?? + "Re-freshing manga data ...", }), ); } diff --git a/src/lib/List/MediaRoulette.svelte b/src/lib/List/MediaRoulette.svelte index 3fbc89d6..dc9a2269 100644 --- a/src/lib/List/MediaRoulette.svelte +++ b/src/lib/List/MediaRoulette.svelte @@ -3,6 +3,7 @@ import type { Media } from "$lib/Data/AniList/media"; import ParallaxImage from "$lib/Image/ParallaxImage.svelte"; import { outboundLink } from "$lib/Media/links"; import settings from "$stores/settings"; +import locale from "$stores/locale"; import { mediaTitle } from "./mediaTitle"; interface Props { @@ -96,14 +97,16 @@ const handleOverlayClick = (e: MouseEvent) => { }} > <div class="roulette-container card {isClosing ? 'fade-out' : 'fade-in'}"> - <button class="close-button" onclick={handleClose} aria-label="Close roulette">×</button> + <button class="close-button" onclick={handleClose} aria-label={$locale().lists.roulette?.closeAriaLabel} + >×</button + > <h3 class="roulette-title"> - {type === 'anime' ? 'Watch' : 'Read'} Roulette + {type === 'anime' ? $locale().lists.roulette?.watchTitle : $locale().lists.roulette?.readTitle} </h3> {#if media.length === 0} - <p>No media available for roulette.</p> + <p>{$locale().lists.roulette?.noMedia}</p> {:else} <div class="roulette-display" class:spinning={isSpinning} class:result={showResult}> {#if currentMedia} @@ -129,13 +132,18 @@ const handleOverlayClick = (e: MouseEvent) => { target="_blank" class="view-link" > - View on {$settings.displayOutboundLinksTo === 'anilist' - ? 'AniList' - : $settings.displayOutboundLinksTo === 'livechartme' - ? 'LiveChart.me' - : $settings.displayOutboundLinksTo === 'animeschedule' - ? 'AnimeSchedule' - : 'MyAnimeList'} + {$locale({ + values: { + site: + $settings.displayOutboundLinksTo === 'anilist' + ? 'AniList' + : $settings.displayOutboundLinksTo === 'livechartme' + ? 'LiveChart.me' + : $settings.displayOutboundLinksTo === 'animeschedule' + ? 'AnimeSchedule' + : 'MyAnimeList' + } + }).lists.roulette?.viewOn} </a> {/if} </div> @@ -145,11 +153,11 @@ const handleOverlayClick = (e: MouseEvent) => { <div class="roulette-actions"> {#if !isSpinning && !showResult} - <button onclick={startRoulette}>Spin!</button> + <button onclick={startRoulette}>{$locale().lists.roulette?.spin}</button> {:else if showResult} - <button onclick={startRoulette}>Spin Again</button> + <button onclick={startRoulette}>{$locale().lists.roulette?.spinAgain}</button> {:else} - <button disabled>Spinning ...</button> + <button disabled>{$locale().lists.roulette?.spinning}</button> {/if} </div> {/if} diff --git a/src/lib/Loading/Message.svelte b/src/lib/Loading/Message.svelte index 36f889d2..4aedbe26 100644 --- a/src/lib/Loading/Message.svelte +++ b/src/lib/Loading/Message.svelte @@ -3,6 +3,7 @@ import Ellipsis from "./Ellipsis.svelte"; import Ripple from "./Ripple.svelte"; import Grid from "./Grid.svelte"; import Popup from "$lib/Layout/Popup.svelte"; +import locale from "$stores/locale"; export let message: string | undefined = undefined; export let loader: "ellipsis" | "ripple" | "grid" = "ellipsis"; @@ -32,7 +33,9 @@ export let fullscreen = true; <slot /> {#if withReload} - Please <a href={'#'} onclick={() => location.reload()}>try again</a> later. + {$locale().hololive?.pleasePrefix ?? 'Please'} + <a href={'#'} onclick={() => location.reload()}>{$locale().common?.tryAgain ?? 'try again'}</a> + {$locale().hololive?.laterSuffix ?? 'later.'} {/if} {/if} </div> diff --git a/src/lib/Locale/english.ts b/src/lib/Locale/english.ts index f0195948..6e46f132 100644 --- a/src/lib/Locale/english.ts +++ b/src/lib/Locale/english.ts @@ -17,6 +17,8 @@ const English: Locale = { hololive: "hololive Schedule", myProfile: "My Profile", myBadgeWall: "My Badge Wall", + menu: "Menu", + avatar: "Avatar", }, settings: { fields: { @@ -33,6 +35,7 @@ const English: Locale = { tooltips: { rss: "Web feed data format", }, + feedUrlLabel: "Your AniList notifications RSS feed URL", }, display: { title: "Display", @@ -60,6 +63,16 @@ const English: Locale = { "Sort anime by difference between last watched and next episode", hint: "By default, anime are sorted by the number of days left until the next episode airs.", }, + hoverCover: + "Show media cover when hovering on supported media titles", + socialButton: "Show social tab shortcut for media", + blurAdult: "Blur NSFW media covers", + copyTitleNotLink: "Copy media title instead of linking", + totalDueEpisodes: + "Display total number of due episodes instead of due media count", + totalEpisodes: "Apply to all media lists, not just due media lists", + scheduleFilterList: + "Only display media on your media lists in Subtitle Schedule", }, }, dateAndTime: { @@ -72,6 +85,8 @@ const English: Locale = { "Show episode countdown in native release date & time", abbreviateCountdown: "Abbreviate episode countdown date & time units", + lastActivityWarningHint: + "A warning will appear at the top of Home and Completed if you have not filled in today's activity history grid point yet. This option is useful to those that like maintaining a consistent activity history grid.", }, }, motionAndAccessibility: { @@ -82,6 +97,12 @@ const English: Locale = { enableAniListNotifications: "Enable AniList notifications", limitPanelAreaToScreenHeight: "Limit panel area to screen height", interfaceLanguage: "Interface language", + aniListNotificationsHint: + "Periodically check for and send recent AniList notifications as native platform notifications. This may be useful for users who have installed due.moe as a PWA or are using due.moe on a mobile device, as AniList has no official mobile app, and the AniList website does not send push notifications.", + aniListNotificationsHint1: + "Periodically check for and send recent AniList notifications as native platform notifications", + aniListNotificationsHint2: + "This may be useful for users who have installed due.moe as a PWA or are using due.moe on a mobile device, as AniList has no official mobile app, and the AniList website does not send push notifications.", }, }, dataSaver: "Data Saver", @@ -104,6 +125,25 @@ const English: Locale = { hint: "Let them remind you. It's for your own benefit.", }, filtersIncludeCompleted: "Affect Completed", + includeAdditionalMediaHint1: + "Media where either the next episode's release date is unknown or the chapter count could not be resolved is considered unresolved.", + includeAdditionalMediaHint2: + "Additionally, you can hard exclude specific media from due.moe on AniList. To exclude any media from being included in any due.moe calculation, create an anime or manga list with the tag #DueIgnore in the list's title and add the media you want to exclude to the list. Inversely, you can selectively include media by creating an anime or manga list with the tag #DueInclude in the list's title, which will include only media in the list in any due.moe calculation. #DueInclude will override #DueIgnore.", + coverModeTitle: "Show lists with media covers instead of text", + coverWidthLabel: "Cover width (px)", + listSortFilterTitle: "List sort & filter", + animeSortOrder: "Anime sort order", + sortOptions: { + timeRemaining: "Time Remaining Until Next Airing Episode", + difference: "Difference Between Progress and Next Airing Episode", + startDate: "Start Date", + endDate: "End Date", + }, + reverseSortOrder: "Reverse anime sort order", + mediaListFilter: "Enable media list filter", + mediaRoulette: "Enable media roulette", + mediaRouletteHint: + "Adds a roulette button to due and completed media lists to randomly pick something to watch or read", }, tooltips: { beta: "Beta", @@ -116,6 +156,7 @@ const English: Locale = { tooltips: { version: "Current due.moe version hash", }, + customCSS: "Custom CSS", }, calculation: { title: "Calculation", @@ -130,21 +171,61 @@ const English: Locale = { }, hideOutOfDateVolumeWarning: { title: "Hide out-of-date volume warning", + hint: "Out-of-date volume warnings display an alert when there is a mismatch between the chapter progress and number of volumes you have logged for a given title. For example, an alert would be shown if you have tracked a manga up to Ch. 50 (Vol. 5), but have less than 4 volumes logged.", + hint1: + "Out-of-date volume warnings display an alert when there is a mismatch between the chapter progress and number of volumes you have logged for a given title.", + hint2: + "For example, an alert would be shown if you have tracked a manga up to Ch. 50 (Vol. 5), but have less than 4 volumes logged.", + speedupHint: + "Disabling this option speeds up refresh times for manga lists.", }, smartChapterCountEstimation: { title: "Enable smart chapter count calculation", + hint: "Smart chapter count calculation uses statistical methods to estimate the number of chapters available for a given title based on user submitted progress. Disabling this setting will disable light novel chapter count reporting and will disable smart chapter count calculation for titles which you have higher progress than officially reported.", + hint1: + "Smart chapter count calculation uses statistical methods to estimate the number of chapters available for a given title based on user submitted progress.", + hint2: + "Disabling this setting will disable light novel chapter count reporting and will disable smart chapter count calculation for titles which you have higher progress than officially reported.", }, preferNativeChapterCount: { title: "Prefer native chapter count", hint: "Prefer comparing against a manga's native chapter count opposed to the translated chapter count", }, + smartChapterMethod: { + label: "Smart chapter count calculation method", + accuracyDisclaimer: + "No chapter count estimation method will be 100% accurate. Since estimated media requires scores derived from user submitted progress, high (or low) false-reports skew the data.", + options: { + mode: "Mode (fast, moderate to low accuracy)", + median: "Median (moderate speed, high accuracy, recommended)", + iqrMedian: + "Interquartile Range with Median (slower, high accuracy)", + iqrMode: "Interquartile Range with Mode (slower, high accuracy)", + }, + }, }, }, cache: { title: "Cache", + clearingNote: "Clearing due.moe's site data will clear these caches too.", + recacheAnimeLabel: "Re-cache AniList media lists every", + recacheMangaLabel: "Re-cache manga data every", + minutes: "minutes", }, attributions: { title: "Attributions", + generalData: + "Most data not explicity attributed otherwise, excluding primary chapter and volume data, character birthday data, and subtitled anime release data", + nonNativeChapter: "non-native chapter and volume count data", + nativeChapter: "Native chapter and volume count data", + girlsBandCryIcons: "Girls Band Cry Icon Set", + outboundDisclaimerTitle: "Outbound Link Disclaimer", + outboundDisclaimerLine1: + "due.moe does not host or directly link to any less-than-legal anime or manga material and/or distribution platforms.", + outboundDisclaimerLine2: + "due.moe is not affiliated with any of the above or below sites and services.", + outboundDisclaimerLine3: + "At the moment, due.moe only ever contains outbound links to the following sites and services:", }, media: { anime: "Anime", @@ -172,6 +253,22 @@ const English: Locale = { disable: "Disable & Keep Local Configuration", delete: "Delete Remote Configuration", }, + lastPush: "Last Push", + lastPull: "Last Pull", + }, + verbiage: { + upcomingEpisodes: + "Anime which you have seen all episodes of thus far, and have a scheduled next episode(s) release date", + notYetReleased: + "Anime which have not yet aired their first episode, and have a scheduled next episode(s) release date", + dueEpisodes: + "Anime which you have not seen all episodes of thus far, and have a scheduled next episode release date", + dueManga: + "Manga which you have not read all chapters of thus far, and have an available next chapter(s)", + completedAnime: + "Anime which you have not seen all episodes of thus far, and have concluded airing", + completedManga: + "Manga which you have not read all chapters of thus far, and have concluded publishing", }, }, user: { @@ -217,6 +314,17 @@ const English: Locale = { statistics: "{username} has watched {anime} days of anime and read {manga} days of manga.", badges: "{username} has collected {badges} badges using Badge Wall.", + notLoaded: "Could not load user profile for @{username}.", + loadingProfile: "Loading user profile ...", + owner: "Owner", + badgeWallLink: "Badge Wall", + pinnedCategories: "Pinned Categories", + categoryPlaceholder: "Category", + biography: "Biography", + markdownPlaceholder: "Markdown supported!", + badgeWallCustomCss: "Badge Wall Custom CSS", + customCssPlaceholder: + "/* Use classes and IDs such as .badges, #badges, .badge, or standard elements like body and details, or anything, as long as it's valid CSS! */", }, preferences: { title: "User Preferences", @@ -260,6 +368,40 @@ const English: Locale = { hint: "Concluded manga and light novels that you have not read all available chapters of", }, }, + empty: { + anime: "No anime to display.", + manga: "No manga to display.", + }, + actions: { + pickRandomAnime: "Pick a random anime to watch", + pickRandomManga: "Pick a random manga to read", + forceRefresh: "Force refresh", + forceFullRefresh: "Force a full refresh", + all: "All", + }, + errors: { + mangaDataDown: + "due.moe's manga data source is currently down for maintenance. Please check back later.", + mangaDataUnavailable: + "due.moe's manga data source is currently unavailable. Please check back later.", + mangaDataUnreachable: + "due.moe's manga data source is currently unreachable. Please check back later.", + }, + dontReadMangaPrompt: + "Don't read manga? You can re-enable it later in the Settings.", + dontReadMangaPrefix: "Don't read manga?", + hideMangaPanel: "Hide the manga panel", + reenableInSettings: "You can re-enable it later in the Settings.", + roulette: { + closeAriaLabel: "Close roulette", + watchTitle: "Watch Roulette", + readTitle: "Read Roulette", + noMedia: "No media available for roulette.", + viewOn: "View on {site}", + spin: "Spin!", + spinAgain: "Spin Again", + spinning: "Spinning ...", + }, }, tools: { tool: { @@ -268,6 +410,110 @@ const English: Locale = { long: "Today's Character Birthdays", }, }, + picker: { + placeholder: "Select a tool to continue", + }, + input: { + pressEnter: "Or click your Enter key", + }, + episodeDiscussion: { + rateLimit: + "Threads could not be loaded. You might have been rate-limited.", + contactSupport: + "Try again in a few minutes. If the problem persists, please contact @fuwn on AniList.", + enterUsername: "Enter a username to search for to continue.", + }, + likes: { + invalidUrl: "Please enter a valid Activity or Thread URL.", + }, + tracker: { + urlTitleRequired: "URL and title are required fields", + entryExists: "Entry with URL already exists: {url}", + confirmDelete: "Click again to confirm deletion", + urlPlaceholder: "URL", + titlePlaceholder: "Title", + progressPlaceholder: "Progress (defaults to 0)", + }, + followFix: { + toggleFor: "Toggle follow for {input}", + }, + sequelSpy: { + winter: "Winter", + spring: "Spring", + summer: "Summer", + fall: "Fall", + countRatio: + "The count ratio is the number of episodes you've seen of any direct prequels, and the total number of episodes of all direct prequels.", + }, + sequelCatcher: { + credit: "Thanks to @sevengirl and @esthereae for the idea!", + includeCurrent: "Include current (watching, rewatching, paused)", + includeSideStories: "Include side stories (e.g., OVAs, specials, etc.)", + }, + activityHistory: { + daysAtRisk: "Days in risk of developing an activity history hole", + daysAtRiskHint: + "Days in which you did not log any activity or only have one activity logged.", + dateLabel: "Date:", + amountLabel: "Amount:", + }, + wrapped: { + highestRated: "Highest Rated", + mostWatched: "Most Watched", + mostRead: "Most Read", + mostCommon: "Most Common", + loadingActivityHistory: "Loading activity history ...", + loadingUserData: "Loading user data ...", + loadingUser: "Loading user ...", + errorFetchingMedia: "Error fetching media.", + multiAttempt: + "With many activities, it may take multiple attempts to obtain all of your activity history from AniList. If this occurs, wait one minute and try again to continue populating your local activity history database.", + multiAttemptPrefix: "With ", + multiAttemptSuffix: + " activities, it may take multiple attempts to obtain all of your activity history from AniList. If this occurs, wait one minute and try again to continue populating your local activity history database.", + clickLoadData: "Click load data!", + saveImageInstruction: + 'Click on the image to download, or right click and select "Save Image As...".', + generateImage: "Generate image", + loadData: "Load data", + reloadData: "Reload data", + display: "Display", + calculation: "Calculation", + advanced: "Advanced", + showWatermark: "Show watermark", + bgTransparency: "Enable background transparency", + lightMode: "Enable light mode", + showGenresTags: "Show top genres and tags", + hideActivityHistory: "Hide activity history", + showRatedPercentages: "Show highest rated media percentages", + showGenreTagPercentages: "Show highest rated genre and tag percentages", + showOngoingPrevious: "Show ongoing media from previous years", + aboveTopRow: "Above Top Row", + belowTopRow: "Below Top Row", + bottom: "Bottom", + activityHistoryPosition: "Activity history position", + highestRatedCount: "Highest rated media count", + highestGenreTagCount: "Highest genre and tag count", + findBestFit: "Find best fit", + widthAdjustment: "Width adjustment", + enableFullYear: "Enable full-year activity", + refreshData: "Refresh data", + calculateForYear: "Calculate for year", + startDateFilter: "Start date filter", + endDateFilter: "End date filter", + animeMangaSort: "Anime and manga sort", + genreTagSort: "Genre and tag sort", + includeMusic: "Include music", + includeRewatches: "Include rewatches & rereads", + includeSpecials: "Include specials", + includeOvas: "Include OVAs", + includeMovies: "Include movies", + excludeUnrated: "Excluded unrated & unwatched", + excludedKeywords: "Excluded keywords", + submit: "Submit", + excludedHint: 'Comma separated list (e.g., "My Hero, Kaguya")', + disableDetailedActivity: "Disable detailed activity information", + }, }, debug: { clearCaches: "Invalidate anime and manga list caches", @@ -277,7 +523,7 @@ const English: Locale = { hint: "Resets all settings present on this page to their default values", }, clearLocalStorage: { - title: "Clear", + title: "Clear local database", hint1: "Resets all of your settings to their default values and clears both AniList media list and manga data caches", hint2: @@ -310,6 +556,12 @@ const English: Locale = { }).format, loadError: "Could not load schedule.", parseError: "hololive timed out.", + noUpcomingStreams: "No upcoming streams.", + loadingSchedule: "Loading schedule ...", + parsingSchedule: "Parsing schedule ...", + tryAgainQuestion: "Try again?", + pleasePrefix: "Please", + laterSuffix: "later.", }, dateFormatter: new Intl.DateTimeFormat("en-US", { year: "numeric", @@ -328,6 +580,237 @@ const English: Locale = { day: "numeric", weekday: "long", }).format, + + common: { + save: "Save", + add: "Add", + remove: "Remove", + submit: "Submit", + cancel: "Cancel", + previous: "Previous", + next: "Next", + search: "Search", + notFound: "Not found", + tryAgain: "try again", + failedToLoad: "Failed to load feed", + minutes: "minutes", + hours: "hours", + loading: "Loading {type} ...{percent}", + }, + errors: { + routeNotFound: "not found", + didYouMean: "Did you mean", + somethingWentWrong: "Something went wrong. Try refreshing.", + toolNotFound: 'Tool "{tool}" not found', + toolNotFoundPrefix: 'Tool "', + toolNotFoundSuffix: '" not found', + profileCouldNotBeLoaded: "@{username}'s profile could not be loaded.", + completedLoginPrompt: "Please log in to view completed media.", + rateLimited: { + notLoaded: "could not be loaded. You might have been rate-limited.", + notLoadedMight: "could not be loaded. You might have been rate-limited.", + notLoadedDefinitely: "could not be loaded. You have been rate-limited.", + tryAgainOneMinute: "Try again in one minute.", + sessionExpired: + "Your login session may have expired. Try logging out and logging back in, or try again in a few minutes.", + contactSupport: + "If the problem persists, please contact @fuwn on AniList.", + }, + animeRateLimited: + "It is likely that you have been rate-limited by AniList. Please try again later.", + loginRequiredPrefix: "Please ", + loginRequiredLink: "log in", + loginRequiredSuffix: " to view this page.", + }, + commandPalette: { + ariaLabel: "Command palette", + placeholder: "Search", + searchCommands: "Search commands", + commands: "Commands", + noResults: "No results found", + refreshCaches: "Refresh Anime & Manga List Caches", + logIn: "Log In", + logOut: "Log Out", + toggles: { + time24On: "Switch to 24-hour time", + time24Off: "Switch to 12-hour time", + animationsOff: "Disable animations", + animationsOn: "Enable animations", + blurAdultOn: "Blur adult content", + blurAdultOff: "Show adult content unblurred", + showAnimeCovers: "Show anime covers", + hideAnimeCovers: "Hide anime covers", + showMangaCovers: "Show manga covers", + hideMangaCovers: "Hide manga covers", + hoverCoverOn: "Enable hover cover preview", + hoverCoverOff: "Disable hover cover preview", + scheduleListMode: "Schedule: list mode", + scheduleGridMode: "Schedule: grid mode", + reverseSort: "Reverse sort order", + restoreSort: "Restore default sort order", + dataSaverOn: "Enable data saver", + dataSaverOff: "Disable data saver", + notificationsOff: "Disable in-app notifications", + notificationsOn: "Enable in-app notifications", + switchLanguageJa: "Switch language to 日本語", + switchLanguageEn: "Switch language to English", + titleFormat: "Title format: {from} → {to}", + outboundLinks: "Outbound links: {from} → {to}", + }, + sync: { + pushNow: "Push Settings Now", + pullNow: "Pull Settings Now", + disable: "Disable Settings Sync", + pushedDescription: "Pushed local configuration to remote", + pulledDescription: "Pulled remote configuration", + noRemoteFound: "No remote configuration found", + disabledHeading: "Settings sync disabled", + }, + }, + headTitle: { + settings: "Settings", + completed: "Completed", + schedule: "Schedule", + tools: "Tools", + profile: "Profile", + updates: "Updates", + girls: "Anime Girls Holding Programming Books", + hololiveSchedule: "hololive Schedule", + userProfile: "{username}'s Profile", + userBadgeWall: "{username}'s Badge Wall", + }, + notifications: { + cacheInvalidated: "Anime and manga list caches successfully invalidated", + mangaHeading: "Manga", + mangaRefreshing: "Re-freshing manga data ...", + recachedFromAniList: "Re-cached media lists from AniList", + settingsReset: "All settings successfully reset", + localDatabaseCleared: "local database successfully cleared", + rssCopied: "RSS feed URL copied to clipboard", + pulledRemote: "Pulled remote configuration", + createdRemote: "Created remote configuration", + pushedRemote: "Pushed local configuration to remote", + syncDisabled: "Settings sync disabled", + remoteDeleted: "Remote configuration deleted and settings sync disabled", + }, + schedule: { + comingSoon: "Coming soon", + continuingFromPreviousSeason: "Continuing from previous season", + loadingSubtitle: "Loading subtitle schedule ...", + loadingSchedule: "Loading schedule ...", + }, + events: { + summary: "Events", + loadingGroups: "Loading groups ...", + parsingGroups: "Parsing groups ...", + errorLoadingGroups: "Error loading groups.", + errorParsingGroups: "Error parsing groups.", + loadingGroup: "Loading group ...", + parsingGroup: "Parsing group ...", + errorLoadingGroup: "Error loading group.", + errorParsingGroup: "Error parsing group.", + groupNotExistPrefix: "This group may not exist. Please ", + groupNotExistSuffix: " later.", + loadingEvents: "Loading events ...", + parsingEvents: "Parsing events ...", + errorParsingEvents: "Error parsing events.", + }, + home: { + lastActivity: { + warning: + "You don't have any new activity statuses from the past day! Create one within {timeLeft} to keep your streak!", + hoursUnit: "hours", + minutesUnit: "minutes", + }, + }, + reader: { + mangaUrl: "Manga URL", + read: "Read", + loadingChapters: "Loading chapters ...", + fetchFailed: "Failed to fetch data", + unknownError: "An unknown error has occurred.", + invalidUrl: "Invalid URL", + vol: "Vol.", + ch: "Ch.", + readFallback: "Read", + }, + routes: { + settingsFeedbackPrefix: + "Have feedback or suggestions? Send a private message to", + settingsFeedbackSuffix: "on AniList!", + toolsFeedbackPrefix: + "Have any requests for cool tools that you think others might find useful? Send a private message to", + toolsFeedbackSuffix: "on AniList!", + girlsTitle: "Anime Girls Holding Programming Books", + girlsIntro: "The Senpy Club | Anime Girls Holding Programming Books", + girlsIntroLeft: "The Senpy Club", + girlsIntroRight: "Anime Girls Holding Programming Books", + girlsRandomAlt: "A random anime girl holding a programming book", + girlsSingleAlt: "An anime girl holding a programming book", + girlsLanguages: "Languages", + girlsLoadingImage: "Loading image ...", + girlsLoadingImages: "Loading images ...", + girlsLoadingLanguages: "Loading languages ...", + updatesManga: "Manga", + updatesNovels: "Novels", + updatesFailedToLoad: "Failed to load feed", + }, + badgePreview: { + designer: "Designer:", + forum: "Forum", + activity: "Activity", + source: "Source:", + category: "Category:", + sauceNAO: "SauceNAO:", + search: "Search", + previous: "Previous", + next: "Next", + }, + badgeWall: { + noRegistered: "No due.moe registered badges found for this user.", + notFound: "Not found", + awcGroup: "Anime Watching Club", + page: { + somethingWentWrong: "Something went wrong. Try refreshing.", + notice: "Notice:", + shadowHide: "Shadow Hide Badges", + unshadowHide: "Un-shadow Hide Badges", + migrateCategory: "Migrate Category", + hideCategory: "Hide Category", + toggleVisibility: "Toggle Visibility", + hidden: "Hidden", + shown: "Shown", + dateTimeHint: + "Must be full date and time, defaults to now if any fields empty", + migrateAllHint: + "Leave category empty to migrate all to or from uncategorised.", + hideVisibilityHint: + "If the majority of the badges in a category are shown, the category will be hidden, and vice versa.", + hideAllHint: "Leave category field empty to hide all.", + loadingBadges: "Loading badges ...", + noBadgesYet: "No badges yet.", + shadowHideNotice1: + "The Badge Wall overseer system has detected badges containing AI-generated material on your wall. {count} of your badges have been shadow hidden.", + shadowHideNotice2: + 'You may use the "Un-shadow Hide Badges" button to unhide these badges, from where you will be required to use the hide feature to hide these badges from the public, while allowing them to stay visible to you as the account holder.', + aiNotice1: + "AniList has begun purging outbound links which contain AI-generated material, this includes Badge Wall. If you have collected badges with AI-generated elements, kindly use the hide feature to hide these badges from the public, while allowing them to stay visible to you as the account holder.", + aiNotice2: + "Failure to comply with this request at your earliest convenience will result in the hiding of all badges from your Badge Wall.", + dismiss: "Dismiss", + loadNoneNoticeBody: + "{count} badges have been loaded successfully, but they are not being displayed due to your preferences ({code}).", + loadNoneNoticePrefix: + "{count} badges have been loaded successfully, but they are not being displayed due to your preferences (", + loadNoneNoticeSuffix: ").", + shadowHideBadge: "Shadow Hide Badge ({id})", + unshadowHideBadge: "Un-shadow Hide Badge ({id})", + migrateAction: "Migrate", + originalCategoryPlaceholder: "Original Category", + newCategoryPlaceholder: "New Category", + categoryPlaceholder: "Category", + }, + }, }; export default English; diff --git a/src/lib/Locale/japanese.ts b/src/lib/Locale/japanese.ts index 8e3744d4..2e683926 100644 --- a/src/lib/Locale/japanese.ts +++ b/src/lib/Locale/japanese.ts @@ -331,6 +331,10 @@ const Japanese: Locale = { day: "numeric", weekday: "long", }).format, + routes: { + settingsFeedbackPrefix: "フィードバックや提案はありますか?AniListで", + settingsFeedbackSuffix: "にDMを送ってください!", + }, }; export default Japanese; diff --git a/src/lib/Locale/layout.ts b/src/lib/Locale/layout.ts index 697404f2..f5f2fbf6 100644 --- a/src/lib/Locale/layout.ts +++ b/src/lib/Locale/layout.ts @@ -23,6 +23,8 @@ export interface Locale { hololive: LocaleValue; myProfile: LocaleValue; myBadgeWall: LocaleValue; + menu?: LocaleValue; + avatar?: LocaleValue; }; settings: { fields: { @@ -39,6 +41,7 @@ export interface Locale { tooltips: { rss: LocaleValue; }; + feedUrlLabel?: LocaleValue; }; display: { title: LocaleValue; @@ -65,6 +68,13 @@ export interface Locale { title: LocaleValue; hint: LocaleValue; }; + hoverCover?: LocaleValue; + socialButton?: LocaleValue; + blurAdult?: LocaleValue; + copyTitleNotLink?: LocaleValue; + totalDueEpisodes?: LocaleValue; + totalEpisodes?: LocaleValue; + scheduleFilterList?: LocaleValue; }; }; dateAndTime: { @@ -75,6 +85,7 @@ export interface Locale { use24HourTime: LocaleValue; nativeEpisodeCountdown: LocaleValue; abbreviateCountdown: LocaleValue; + lastActivityWarningHint?: LocaleValue; }; }; motionAndAccessibility: { @@ -85,6 +96,9 @@ export interface Locale { enableAniListNotifications: LocaleValue; limitPanelAreaToScreenHeight: LocaleValue; interfaceLanguage: LocaleValue; + aniListNotificationsHint?: LocaleValue; + aniListNotificationsHint1?: LocaleValue; + aniListNotificationsHint2?: LocaleValue; }; }; dataSaver: LocaleValue; @@ -107,6 +121,22 @@ export interface Locale { hint: LocaleValue; }; filtersIncludeCompleted: LocaleValue; + includeAdditionalMediaHint1?: LocaleValue; + includeAdditionalMediaHint2?: LocaleValue; + coverModeTitle?: LocaleValue; + coverWidthLabel?: LocaleValue; + listSortFilterTitle?: LocaleValue; + animeSortOrder?: LocaleValue; + sortOptions?: { + timeRemaining?: LocaleValue; + difference?: LocaleValue; + startDate?: LocaleValue; + endDate?: LocaleValue; + }; + reverseSortOrder?: LocaleValue; + mediaListFilter?: LocaleValue; + mediaRoulette?: LocaleValue; + mediaRouletteHint?: LocaleValue; }; tooltips: { beta: LocaleValue; @@ -118,6 +148,7 @@ export interface Locale { tooltips: { version: LocaleValue; }; + customCSS?: LocaleValue; }; calculation: { title: LocaleValue; @@ -132,21 +163,50 @@ export interface Locale { }; hideOutOfDateVolumeWarning: { title: LocaleValue; + hint?: LocaleValue; + hint1?: LocaleValue; + hint2?: LocaleValue; + speedupHint?: LocaleValue; }; smartChapterCountEstimation: { title: LocaleValue; + hint?: LocaleValue; + hint1?: LocaleValue; + hint2?: LocaleValue; }; preferNativeChapterCount: { title: LocaleValue; hint: LocaleValue; }; + smartChapterMethod?: { + label?: LocaleValue; + accuracyDisclaimer?: LocaleValue; + options?: { + mode?: LocaleValue; + median?: LocaleValue; + iqrMedian?: LocaleValue; + iqrMode?: LocaleValue; + }; + }; }; }; cache: { title: LocaleValue; + clearingNote?: LocaleValue; + recacheAnimeLabel?: LocaleValue; + recacheMangaLabel?: LocaleValue; + minutes?: LocaleValue; }; attributions: { title: LocaleValue; + generalData?: LocaleValue; + nonNativeChapter?: LocaleValue; + nativeChapter?: LocaleValue; + girlsBandCryIcons?: LocaleValue; + outboundDisclaimerTitle?: LocaleValue; + outboundDisclaimerLine1?: LocaleValue; + outboundDisclaimerLine2?: LocaleValue; + outboundDisclaimerLine3?: LocaleValue; }; media: { anime: LocaleValue; @@ -174,6 +234,16 @@ export interface Locale { disable: LocaleValue; delete: LocaleValue; }; + lastPush?: LocaleValue; + lastPull?: LocaleValue; + }; + verbiage?: { + upcomingEpisodes?: LocaleValue; + notYetReleased?: LocaleValue; + dueEpisodes?: LocaleValue; + dueManga?: LocaleValue; + completedAnime?: LocaleValue; + completedManga?: LocaleValue; }; }; user: { @@ -217,6 +287,16 @@ export interface Locale { profile: { statistics: LocaleValue; badges: LocaleValue; + notLoaded?: LocaleValue; + loadingProfile?: LocaleValue; + owner?: LocaleValue; + badgeWallLink?: LocaleValue; + pinnedCategories?: LocaleValue; + categoryPlaceholder?: LocaleValue; + biography?: LocaleValue; + markdownPlaceholder?: LocaleValue; + badgeWallCustomCss?: LocaleValue; + customCssPlaceholder?: LocaleValue; }; preferences: { title: LocaleValue; @@ -260,6 +340,36 @@ export interface Locale { hint: LocaleValue; }; }; + empty?: { + anime?: LocaleValue; + manga?: LocaleValue; + }; + actions?: { + pickRandomAnime?: LocaleValue; + pickRandomManga?: LocaleValue; + forceRefresh?: LocaleValue; + forceFullRefresh?: LocaleValue; + all?: LocaleValue; + }; + errors?: { + mangaDataDown?: LocaleValue; + mangaDataUnavailable?: LocaleValue; + mangaDataUnreachable?: LocaleValue; + }; + dontReadMangaPrompt?: LocaleValue; + dontReadMangaPrefix?: LocaleValue; + hideMangaPanel?: LocaleValue; + reenableInSettings?: LocaleValue; + roulette?: { + closeAriaLabel?: LocaleValue; + watchTitle?: LocaleValue; + readTitle?: LocaleValue; + noMedia?: LocaleValue; + viewOn?: LocaleValue; + spin?: LocaleValue; + spinAgain?: LocaleValue; + spinning?: LocaleValue; + }; }; tools: { tool: { @@ -268,6 +378,103 @@ export interface Locale { long: LocaleValue; }; }; + picker?: { + placeholder?: LocaleValue; + }; + input?: { + pressEnter?: LocaleValue; + }; + episodeDiscussion?: { + rateLimit?: LocaleValue; + contactSupport?: LocaleValue; + enterUsername?: LocaleValue; + }; + likes?: { + invalidUrl?: LocaleValue; + }; + tracker?: { + urlTitleRequired?: LocaleValue; + entryExists?: LocaleValue; + confirmDelete?: LocaleValue; + urlPlaceholder?: LocaleValue; + titlePlaceholder?: LocaleValue; + progressPlaceholder?: LocaleValue; + }; + followFix?: { + toggleFor?: LocaleValue; + }; + sequelSpy?: { + winter?: LocaleValue; + spring?: LocaleValue; + summer?: LocaleValue; + fall?: LocaleValue; + countRatio?: LocaleValue; + }; + sequelCatcher?: { + credit?: LocaleValue; + includeCurrent?: LocaleValue; + includeSideStories?: LocaleValue; + }; + activityHistory?: { + daysAtRisk?: LocaleValue; + daysAtRiskHint?: LocaleValue; + dateLabel?: LocaleValue; + amountLabel?: LocaleValue; + }; + wrapped?: { + highestRated?: LocaleValue; + mostWatched?: LocaleValue; + mostRead?: LocaleValue; + mostCommon?: LocaleValue; + loadingActivityHistory?: LocaleValue; + loadingUserData?: LocaleValue; + loadingUser?: LocaleValue; + errorFetchingMedia?: LocaleValue; + multiAttempt?: LocaleValue; + multiAttemptPrefix?: LocaleValue; + multiAttemptSuffix?: LocaleValue; + clickLoadData?: LocaleValue; + saveImageInstruction?: LocaleValue; + generateImage?: LocaleValue; + loadData?: LocaleValue; + reloadData?: LocaleValue; + display?: LocaleValue; + calculation?: LocaleValue; + advanced?: LocaleValue; + showWatermark?: LocaleValue; + bgTransparency?: LocaleValue; + lightMode?: LocaleValue; + showGenresTags?: LocaleValue; + hideActivityHistory?: LocaleValue; + showRatedPercentages?: LocaleValue; + showGenreTagPercentages?: LocaleValue; + showOngoingPrevious?: LocaleValue; + aboveTopRow?: LocaleValue; + belowTopRow?: LocaleValue; + bottom?: LocaleValue; + activityHistoryPosition?: LocaleValue; + highestRatedCount?: LocaleValue; + highestGenreTagCount?: LocaleValue; + findBestFit?: LocaleValue; + widthAdjustment?: LocaleValue; + enableFullYear?: LocaleValue; + refreshData?: LocaleValue; + calculateForYear?: LocaleValue; + startDateFilter?: LocaleValue; + endDateFilter?: LocaleValue; + animeMangaSort?: LocaleValue; + genreTagSort?: LocaleValue; + includeMusic?: LocaleValue; + includeRewatches?: LocaleValue; + includeSpecials?: LocaleValue; + includeOvas?: LocaleValue; + includeMovies?: LocaleValue; + excludeUnrated?: LocaleValue; + excludedKeywords?: LocaleValue; + submit?: LocaleValue; + excludedHint?: LocaleValue; + disableDetailedActivity?: LocaleValue; + }; }; debug: { clearCaches: LocaleValue; @@ -298,7 +505,229 @@ export interface Locale { dateFormatter: (date?: number | Date | undefined) => string; loadError: LocaleValue; parseError: LocaleValue; + noUpcomingStreams?: LocaleValue; + loadingSchedule?: LocaleValue; + parsingSchedule?: LocaleValue; + tryAgainQuestion?: LocaleValue; + pleasePrefix?: LocaleValue; + laterSuffix?: LocaleValue; }; dateFormatter: (date?: number | Date | undefined) => string; dayFormatter: (date?: number | Date | undefined) => string; + + common?: { + save?: LocaleValue; + add?: LocaleValue; + remove?: LocaleValue; + submit?: LocaleValue; + cancel?: LocaleValue; + previous?: LocaleValue; + next?: LocaleValue; + search?: LocaleValue; + notFound?: LocaleValue; + tryAgain?: LocaleValue; + failedToLoad?: LocaleValue; + minutes?: LocaleValue; + hours?: LocaleValue; + loading?: LocaleValue; + }; + errors?: { + routeNotFound?: LocaleValue; + didYouMean?: LocaleValue; + somethingWentWrong?: LocaleValue; + toolNotFound?: LocaleValue; + toolNotFoundPrefix?: LocaleValue; + toolNotFoundSuffix?: LocaleValue; + profileCouldNotBeLoaded?: LocaleValue; + completedLoginPrompt?: LocaleValue; + rateLimited?: { + notLoaded?: LocaleValue; + notLoadedMight?: LocaleValue; + notLoadedDefinitely?: LocaleValue; + tryAgainOneMinute?: LocaleValue; + sessionExpired?: LocaleValue; + contactSupport?: LocaleValue; + }; + animeRateLimited?: LocaleValue; + loginRequiredPrefix?: LocaleValue; + loginRequiredLink?: LocaleValue; + loginRequiredSuffix?: LocaleValue; + }; + commandPalette?: { + ariaLabel?: LocaleValue; + placeholder?: LocaleValue; + searchCommands?: LocaleValue; + commands?: LocaleValue; + noResults?: LocaleValue; + refreshCaches?: LocaleValue; + logIn?: LocaleValue; + logOut?: LocaleValue; + toggles?: { + time24On?: LocaleValue; + time24Off?: LocaleValue; + animationsOff?: LocaleValue; + animationsOn?: LocaleValue; + blurAdultOn?: LocaleValue; + blurAdultOff?: LocaleValue; + showAnimeCovers?: LocaleValue; + hideAnimeCovers?: LocaleValue; + showMangaCovers?: LocaleValue; + hideMangaCovers?: LocaleValue; + hoverCoverOn?: LocaleValue; + hoverCoverOff?: LocaleValue; + scheduleListMode?: LocaleValue; + scheduleGridMode?: LocaleValue; + reverseSort?: LocaleValue; + restoreSort?: LocaleValue; + dataSaverOn?: LocaleValue; + dataSaverOff?: LocaleValue; + notificationsOff?: LocaleValue; + notificationsOn?: LocaleValue; + switchLanguageJa?: LocaleValue; + switchLanguageEn?: LocaleValue; + titleFormat?: LocaleValue; + outboundLinks?: LocaleValue; + }; + sync?: { + pushNow?: LocaleValue; + pullNow?: LocaleValue; + disable?: LocaleValue; + pushedDescription?: LocaleValue; + pulledDescription?: LocaleValue; + noRemoteFound?: LocaleValue; + disabledHeading?: LocaleValue; + }; + }; + headTitle?: { + settings?: LocaleValue; + completed?: LocaleValue; + schedule?: LocaleValue; + tools?: LocaleValue; + profile?: LocaleValue; + updates?: LocaleValue; + girls?: LocaleValue; + hololiveSchedule?: LocaleValue; + userProfile?: LocaleValue; + userBadgeWall?: LocaleValue; + }; + notifications?: { + cacheInvalidated?: LocaleValue; + mangaHeading?: LocaleValue; + mangaRefreshing?: LocaleValue; + recachedFromAniList?: LocaleValue; + settingsReset?: LocaleValue; + localDatabaseCleared?: LocaleValue; + rssCopied?: LocaleValue; + pulledRemote?: LocaleValue; + createdRemote?: LocaleValue; + pushedRemote?: LocaleValue; + syncDisabled?: LocaleValue; + remoteDeleted?: LocaleValue; + }; + schedule?: { + comingSoon?: LocaleValue; + continuingFromPreviousSeason?: LocaleValue; + loadingSubtitle?: LocaleValue; + loadingSchedule?: LocaleValue; + }; + events?: { + summary?: LocaleValue; + loadingGroups?: LocaleValue; + parsingGroups?: LocaleValue; + errorLoadingGroups?: LocaleValue; + errorParsingGroups?: LocaleValue; + loadingGroup?: LocaleValue; + parsingGroup?: LocaleValue; + errorLoadingGroup?: LocaleValue; + errorParsingGroup?: LocaleValue; + groupNotExistPrefix?: LocaleValue; + groupNotExistSuffix?: LocaleValue; + loadingEvents?: LocaleValue; + parsingEvents?: LocaleValue; + errorParsingEvents?: LocaleValue; + }; + home?: { + lastActivity?: { + warning?: LocaleValue; + hoursUnit?: LocaleValue; + minutesUnit?: LocaleValue; + }; + }; + reader?: { + mangaUrl?: LocaleValue; + read?: LocaleValue; + loadingChapters?: LocaleValue; + fetchFailed?: LocaleValue; + unknownError?: LocaleValue; + invalidUrl?: LocaleValue; + vol?: LocaleValue; + ch?: LocaleValue; + readFallback?: LocaleValue; + }; + routes?: { + settingsFeedbackPrefix?: LocaleValue; + settingsFeedbackSuffix?: LocaleValue; + toolsFeedbackPrefix?: LocaleValue; + toolsFeedbackSuffix?: LocaleValue; + girlsTitle?: LocaleValue; + girlsIntro?: LocaleValue; + girlsIntroLeft?: LocaleValue; + girlsIntroRight?: LocaleValue; + girlsRandomAlt?: LocaleValue; + girlsSingleAlt?: LocaleValue; + girlsLanguages?: LocaleValue; + girlsLoadingImage?: LocaleValue; + girlsLoadingImages?: LocaleValue; + girlsLoadingLanguages?: LocaleValue; + updatesManga?: LocaleValue; + updatesNovels?: LocaleValue; + updatesFailedToLoad?: LocaleValue; + }; + badgePreview?: { + designer?: LocaleValue; + forum?: LocaleValue; + activity?: LocaleValue; + source?: LocaleValue; + category?: LocaleValue; + sauceNAO?: LocaleValue; + search?: LocaleValue; + previous?: LocaleValue; + next?: LocaleValue; + }; + badgeWall?: { + noRegistered?: LocaleValue; + notFound?: LocaleValue; + awcGroup?: LocaleValue; + page?: { + somethingWentWrong?: LocaleValue; + notice?: LocaleValue; + shadowHide?: LocaleValue; + unshadowHide?: LocaleValue; + migrateCategory?: LocaleValue; + hideCategory?: LocaleValue; + toggleVisibility?: LocaleValue; + hidden?: LocaleValue; + shown?: LocaleValue; + dateTimeHint?: LocaleValue; + migrateAllHint?: LocaleValue; + hideVisibilityHint?: LocaleValue; + hideAllHint?: LocaleValue; + loadingBadges?: LocaleValue; + noBadgesYet?: LocaleValue; + shadowHideNotice1?: LocaleValue; + shadowHideNotice2?: LocaleValue; + aiNotice1?: LocaleValue; + aiNotice2?: LocaleValue; + dismiss?: LocaleValue; + loadNoneNoticeBody?: LocaleValue; + loadNoneNoticePrefix?: LocaleValue; + loadNoneNoticeSuffix?: LocaleValue; + shadowHideBadge?: LocaleValue; + unshadowHideBadge?: LocaleValue; + migrateAction?: LocaleValue; + originalCategoryPlaceholder?: LocaleValue; + newCategoryPlaceholder?: LocaleValue; + categoryPlaceholder?: LocaleValue; + }; + }; } diff --git a/src/lib/Media/invalidate.ts b/src/lib/Media/invalidate.ts index 3a9ff067..2db4b9af 100644 --- a/src/lib/Media/invalidate.ts +++ b/src/lib/Media/invalidate.ts @@ -3,6 +3,8 @@ import { addNotification } from "$lib/Notification/store"; import { options } from "$lib/Notification/options"; import revalidateAnime from "$stores/revalidateAnime"; import revalidateManga from "$stores/revalidateManga"; +import locale from "$stores/locale"; +import { get } from "svelte/store"; export const invalidateListCaches = () => { if (!browser) return; @@ -12,7 +14,9 @@ export const invalidateListCaches = () => { addNotification( options({ - heading: "Anime and manga list caches successfully invalidated", + heading: + get(locale)().notifications?.cacheInvalidated ?? + "Anime and manga list caches successfully invalidated", }), ); }; diff --git a/src/lib/Reader/Chapters/MangaDex.svelte b/src/lib/Reader/Chapters/MangaDex.svelte index 2bbf076f..8a6fe47c 100644 --- a/src/lib/Reader/Chapters/MangaDex.svelte +++ b/src/lib/Reader/Chapters/MangaDex.svelte @@ -1,4 +1,6 @@ <script lang="ts"> +import locale from "$stores/locale"; + interface MangaDexChapter { id: string; attributes: { @@ -19,16 +21,16 @@ export let data: MangaDexData; {#each data.data as chapter} <li> {#if chapter.attributes.volume} - Vol. {chapter.attributes.volume} + {$locale().reader?.vol} {chapter.attributes.volume} {/if} - Ch. {chapter.attributes.chapter} + {$locale().reader?.ch} {chapter.attributes.chapter} <span class="opaque">|</span> <a href={`https://mangadex.org/chapter/${chapter.id}`} target="_blank" rel="noopener noreferrer" > - {chapter.attributes.title || 'Read'} + {chapter.attributes.title || $locale().reader?.readFallback || 'Read'} </a> </li> {/each} diff --git a/src/lib/Schedule/Crunchyroll.svelte b/src/lib/Schedule/Crunchyroll.svelte index 1f5f121c..a3adeb60 100644 --- a/src/lib/Schedule/Crunchyroll.svelte +++ b/src/lib/Schedule/Crunchyroll.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import Spacer from "$lib/Layout/Spacer.svelte"; import crunchyroll from "$lib/Data/Static/crunchyroll.json"; +import locale from "$stores/locale"; import "./container.css"; interface CrunchyrollMedia<T = number | "soon" | "continuing"> { @@ -71,7 +72,7 @@ const ordinalSuffix = (i: number) => { <div class="card day"> <details open class="details-unstyled"> - <summary>Coming soon</summary> + <summary>{$locale().schedule?.comingSoon}</summary> <ol> {#each soon as media} @@ -85,7 +86,7 @@ const ordinalSuffix = (i: number) => { <div class="card day"> <details open class="details-unstyled"> - <summary>Continuing from previous season</summary> + <summary>{$locale().schedule?.continuingFromPreviousSeason}</summary> <ol> {#each continuing as media} diff --git a/src/lib/Settings/Categories/Attributions.svelte b/src/lib/Settings/Categories/Attributions.svelte index ef78a0c1..131bd90a 100644 --- a/src/lib/Settings/Categories/Attributions.svelte +++ b/src/lib/Settings/Categories/Attributions.svelte @@ -1,20 +1,23 @@ <script> import Spacer from "$lib/Layout/Spacer.svelte"; import root from "$lib/Utility/root"; +import locale from "$stores/locale"; </script> <ul> <li> - <a href="https://anilist.co/" target="_blank">AniList</a>: Most data not explicity attributed - otherwise, excluding primary chapter and volume data, character birthday data, and subtitled - anime release data + <a href="https://anilist.co/" target="_blank">AniList</a>: {$locale().settings.attributions.generalData} </li> - <li><span id="mangadex">MangaDex</span>: <b>Only</b> non-native chapter and volume count data</li> <li> - <a href="https://seiga.nicovideo.jp/manga/" target="_blank">ニコニコ漫画</a>: Native chapter and - volume count data + <span id="mangadex">MangaDex</span>: <b>Only</b> + {$locale().settings.attributions.nonNativeChapter} + </li> + <li> + <a href="https://seiga.nicovideo.jp/manga/" target="_blank">ニコニコ漫画</a>: {$locale().settings.attributions.nativeChapter} + </li> + <li> + <a href="https://x.com/YDPFALION" target="_blank">YDPFa</a>: {$locale().settings.attributions.girlsBandCryIcons} </li> - <li><a href="https://x.com/YDPFALION" target="_blank">YDPFa</a>: Girls Band Cry Icon Set</li> <!-- <li> <a href="https://www.animecharactersdatabase.com/index.php" target="_blank"> Anime Characters Database @@ -38,20 +41,20 @@ import root from "$lib/Utility/root"; <Spacer /> <details open class="card-clear"> - <summary>Outbound Link Disclaimer</summary> + <summary>{$locale().settings.attributions.outboundDisclaimerTitle}</summary> <ol> <li> <a href={root('/')}>due.moe</a> - does not host or directly link to any less-than-legal anime or manga material and/or distribution - platforms. + {$locale().settings.attributions.outboundDisclaimerLine1?.replace('due.moe ', '')} </li> <li> <a href={root('/')}>due.moe</a> - is not affiliated with any of the above or below sites and services. + {$locale().settings.attributions.outboundDisclaimerLine2?.replace('due.moe ', '')} </li> <li> - At the moment, <a href={root('/')}>due.moe</a> only ever contains outbound links to the - following sites and services: + {$locale().settings.attributions.outboundDisclaimerLine3?.split('due.moe')[0]}<a + href={root('/')}>due.moe</a + >{$locale().settings.attributions.outboundDisclaimerLine3?.split('due.moe')[1]} <ul> <li><a href="https://anilist.co/">AniList.co</a></li> diff --git a/src/lib/Settings/Categories/Cache.svelte b/src/lib/Settings/Categories/Cache.svelte index 6af897c9..0df59db2 100644 --- a/src/lib/Settings/Categories/Cache.svelte +++ b/src/lib/Settings/Categories/Cache.svelte @@ -1,16 +1,16 @@ <script> import Spacer from "$lib/Layout/Spacer.svelte"; import settings from "$stores/settings"; +import locale from "$stores/locale"; </script> <small class="opaque"> - Clearing - <a href="https://due.moe">due.moe</a>'s site data will clear these caches too. + {$locale().settings.cache.clearingNote?.split('due.moe')[0]}<a href="https://due.moe">due.moe</a>{$locale().settings.cache.clearingNote?.split('due.moe')[1]} </small> <Spacer /> -Re-cache AniList media lists every +{$locale().settings.cache.recacheAnimeLabel} <input type="number" class="no-shadow" @@ -23,11 +23,11 @@ Re-cache AniList media lists every ($settings.cacheMinutes < 1 && ($settings.cacheMinutes = 1)) || ($settings.cacheMinutes > 60 && ($settings.cacheMinutes = 60))} /> -minutes +{$locale().settings.cache.minutes} <br /> -Re-cache manga data every +{$locale().settings.cache.recacheMangaLabel} <input type="number" class="no-shadow" @@ -40,4 +40,4 @@ Re-cache manga data every ($settings.cacheMangaMinutes < 5 && ($settings.cacheMangaMinutes = 5)) || ($settings.cacheMangaMinutes > 1440 && ($settings.cacheMangaMinutes = 1440))} /> -minutes +{$locale().settings.cache.minutes} diff --git a/src/lib/Settings/Categories/Calculation.svelte b/src/lib/Settings/Categories/Calculation.svelte index b4c76269..7106c266 100644 --- a/src/lib/Settings/Categories/Calculation.svelte +++ b/src/lib/Settings/Categories/Calculation.svelte @@ -45,15 +45,13 @@ import SettingHint from "../SettingHint.svelte"; text={$locale().settings.calculation.fields.hideOutOfDateVolumeWarning.title} > <SettingHint lineBreak> - Out-of-date volume warnings display an alert when there is a mismatch between the chapter - progress and number of volumes you have logged for a given title. + {$locale().settings.calculation.fields.hideOutOfDateVolumeWarning.hint1} <br /> - For example, an alert would be shown if you have tracked a manga up to Ch. 50 (Vol. 5), but have less - than 4 volumes logged. + {$locale().settings.calculation.fields.hideOutOfDateVolumeWarning.hint2} <br /> - Disabling this option speeds up refresh times for manga lists. + {$locale().settings.calculation.fields.hideOutOfDateVolumeWarning.speedupHint} </SettingHint> </SettingCheckboxToggle> @@ -65,10 +63,8 @@ import SettingHint from "../SettingHint.svelte"; invert > <SettingHint lineBreak> - Smart chapter count calculation uses statistical methods to estimate the number of chapters - available for a given title based on user submitted progress.<br />Disabling this setting will - disable light novel chapter count reporting and will disable smart chapter count calculation for - titles which you have higher progress than officially reported. + {$locale().settings.calculation.fields.smartChapterCountEstimation.hint1}<br + />{$locale().settings.calculation.fields.smartChapterCountEstimation.hint2} </SettingHint> </SettingCheckboxToggle> @@ -76,15 +72,22 @@ import SettingHint from "../SettingHint.svelte"; <br /> <select bind:value={$settings.calculateGuessMethod} onchange={pruneAllManga}> - <option value="mode">Mode (fast, moderate to low accuracy)</option> - <option value="median">Median (moderate speed, high accuracy, recommended)</option> - <option value="iqr_median">Interquartile Range with Median (slower, high accuracy)</option> - <option value="iqr_mode">Interquartile Range with Mode (slower, high accuracy)</option> + <option value="mode" + >{$locale().settings.calculation.fields.smartChapterMethod?.options?.mode}</option + > + <option value="median" + >{$locale().settings.calculation.fields.smartChapterMethod?.options?.median}</option + > + <option value="iqr_median" + >{$locale().settings.calculation.fields.smartChapterMethod?.options?.iqrMedian}</option + > + <option value="iqr_mode" + >{$locale().settings.calculation.fields.smartChapterMethod?.options?.iqrMode}</option + > </select> - Smart chapter count calculation method + {$locale().settings.calculation.fields.smartChapterMethod?.label} <SettingHint lineBreak> - No chapter count estimation method will be 100% accurate. Since estimated media requires scores - derived from user submitted progress, high (or low) false-reports skew the data. + {$locale().settings.calculation.fields.smartChapterMethod?.accuracyDisclaimer} </SettingHint> {/if} diff --git a/src/lib/Settings/Categories/Debug.svelte b/src/lib/Settings/Categories/Debug.svelte index 6da4b6ae..a2cb35c5 100644 --- a/src/lib/Settings/Categories/Debug.svelte +++ b/src/lib/Settings/Categories/Debug.svelte @@ -8,6 +8,7 @@ import locale from "$stores/locale"; import SettingCheckboxToggle from "../SettingCheckboxToggle.svelte"; import localforage from "localforage"; import { invalidateListCaches } from "$lib/Media/invalidate"; +import { get } from "svelte/store"; </script> <SettingCheckboxToggle setting="debugDummyLists" text={$locale().debug.dummyLists} /> @@ -33,7 +34,7 @@ import { invalidateListCaches } from "$lib/Media/invalidate"; settings.reset(); addNotification( options({ - heading: 'All settings successfully reset' + heading: get(locale)().notifications?.settingsReset ?? 'All settings successfully reset' }) ); }} @@ -51,10 +52,10 @@ import { invalidateListCaches } from "$lib/Media/invalidate"; await localforage.clear(); addNotification( options({ - heading: 'local database successfully cleared' + heading: get(locale)().notifications?.localDatabaseCleared ?? 'local database successfully cleared' }) ); - }}>{$locale().debug.clearLocalStorage.title} local database</button + }}>{$locale().debug.clearLocalStorage.title}</button > <SettingHint lineBreak> {$locale().debug.clearLocalStorage.hint1} @@ -64,5 +65,5 @@ import { invalidateListCaches } from "$lib/Media/invalidate"; <Spacer /> -Custom CSS +{$locale().settings.debug.customCSS} <textarea bind:value={$settings.displayCustomCSS}></textarea> diff --git a/src/lib/Settings/Categories/Display.svelte b/src/lib/Settings/Categories/Display.svelte index 7de57c5a..80f21fde 100644 --- a/src/lib/Settings/Categories/Display.svelte +++ b/src/lib/Settings/Categories/Display.svelte @@ -221,10 +221,9 @@ const onHelperChange = () => { }} > <SettingHint lineBreak> - Periodically check for and send recent AniList notifications as native platform notifications + {$locale().settings.display.categories.motionAndAccessibility.fields.aniListNotificationsHint1} <br /> - This may be useful for users who have installed due.moe as a PWA or are using due.moe on a mobile - device, as AniList has no official mobile app, and the AniList website does not send push notifications. + {$locale().settings.display.categories.motionAndAccessibility.fields.aniListNotificationsHint2} </SettingHint> </SettingCheckboxToggle> @@ -265,12 +264,20 @@ const onHelperChange = () => { <Spacer /> -<b>Show lists with media covers instead of text</b><br /> -<SettingCheckboxToggle setting="displayCoverModeAnime" text="Anime" lineBreak={false} /> -<SettingCheckboxToggle setting="displayCoverModeManga" text="Manga" lineBreak={false} /> +<b>{$locale().settings.display.categories.coverModeTitle}</b><br /> +<SettingCheckboxToggle + setting="displayCoverModeAnime" + text={$locale().settings.media.anime} + lineBreak={false} +/> +<SettingCheckboxToggle + setting="displayCoverModeManga" + text={$locale().settings.media.manga} + lineBreak={false} +/> <SettingCheckboxToggle setting="displayScheduleListMode" - text="Subtitle Schedule" + text={$locale().navigation.subtitleSchedule} lineBreak={false} invert /> @@ -293,7 +300,7 @@ const onHelperChange = () => { $settings.displayCoverWidth < 50 && ($settings.displayCoverWidth = 50); }} /> - Cover width (px) + {$locale().settings.display.categories.coverWidthLabel} <br /> {:else} <br /> @@ -301,22 +308,34 @@ const onHelperChange = () => { <Spacer /> -<b>List sort & filter</b><br /> +<b>{$locale().settings.display.categories.listSortFilterTitle}</b><br /> <select bind:value={$settings.displayAnimeSort}> - <option value="time_remaining">Time Remaining Until Next Airing Episode</option> - <option value="difference">Difference Between Progress and Next Airing Episode</option> - <option value="start_date">Start Date</option> - <option value="end_date">End Date</option> + <option value="time_remaining" + >{$locale().settings.display.categories.sortOptions?.timeRemaining}</option + > + <option value="difference" + >{$locale().settings.display.categories.sortOptions?.difference}</option + > + <option value="start_date">{$locale().settings.display.categories.sortOptions?.startDate}</option> + <option value="end_date">{$locale().settings.display.categories.sortOptions?.endDate}</option> </select> -Anime sort order +{$locale().settings.display.categories.animeSortOrder} <br /> -<SettingCheckboxToggle setting="displayReverseSort" text="Reverse anime sort order" /> -<SettingCheckboxToggle setting="displayMediaListFilter" text="Enable media list filter" /> +<SettingCheckboxToggle + setting="displayReverseSort" + text={$locale().settings.display.categories.reverseSortOrder} +/> +<SettingCheckboxToggle + setting="displayMediaListFilter" + text={$locale().settings.display.categories.mediaListFilter} +/> <br /> -<SettingCheckboxToggle setting="displayMediaRoulette" text="Enable media roulette"> +<SettingCheckboxToggle + setting="displayMediaRoulette" + text={$locale().settings.display.categories.mediaRoulette} +> <SettingHint lineBreak> - Adds a roulette button to due and completed media lists to randomly pick something to watch or - read + {$locale().settings.display.categories.mediaRouletteHint} </SettingHint> </SettingCheckboxToggle> <br /> @@ -324,30 +343,36 @@ Anime sort order <b>{$locale().settings.display.categories.media.title}</b><br /> <SettingCheckboxToggle setting="displayHoverCover" - text="Show media cover when hovering on supported media titles" + text={$locale().settings.display.categories.media.fields.hoverCover} +/> +<SettingCheckboxToggle + setting="displaySocialButton" + text={$locale().settings.display.categories.media.fields.socialButton} +/> +<SettingCheckboxToggle + setting="displayBlurAdultContent" + text={$locale().settings.display.categories.media.fields.blurAdult} /> -<SettingCheckboxToggle setting="displaySocialButton" text="Show social tab shortcut for media" /> -<SettingCheckboxToggle setting="displayBlurAdultContent" text="Blur NSFW media covers" /> <SettingCheckboxToggle setting="displayCopyMediaTitleNotLink" - text="Copy media title instead of linking" + text={$locale().settings.display.categories.media.fields.copyTitleNotLink} /> <SettingCheckboxToggle setting="displayTotalDueEpisodes" - text="Display total number of due episodes instead of due media count" + text={$locale().settings.display.categories.media.fields.totalDueEpisodes} lineBreak={!$settings.displayTotalDueEpisodes} /> {#if $settings.displayTotalDueEpisodes} <SettingCheckboxToggle setting="displayTotalEpisodes" - text="Apply to all media lists, not just due media lists" + text={$locale().settings.display.categories.media.fields.totalEpisodes} /> {/if} <SettingCheckboxToggle setting="displayScheduleFilterList" - text="Only display media on your media lists in Subtitle Schedule" + text={$locale().settings.display.categories.media.fields.scheduleFilterList} id="schedule-filter-list" /> <select bind:value={$settings.displayTitleFormat}> diff --git a/src/lib/Settings/Categories/RSSFeeds.svelte b/src/lib/Settings/Categories/RSSFeeds.svelte index fee411f4..08ba7292 100644 --- a/src/lib/Settings/Categories/RSSFeeds.svelte +++ b/src/lib/Settings/Categories/RSSFeeds.svelte @@ -5,6 +5,7 @@ import { appOrigin } from "$lib/Utility/appOrigin"; import locale from "$stores/locale"; import SettingHint from "../SettingHint.svelte"; import tooltip from "$lib/Tooltip/tooltip"; +import { get } from "svelte/store"; export let user: { accessToken: string; refreshToken: string }; </script> @@ -13,7 +14,7 @@ export let user: { accessToken: string; refreshToken: string }; onclick={() => { addNotification( options({ - heading: 'RSS feed URL copied to clipboard' + heading: get(locale)().notifications?.rssCopied ?? 'RSS feed URL copied to clipboard' }) ); navigator.clipboard.writeText( @@ -23,7 +24,7 @@ export let user: { accessToken: string; refreshToken: string }; > {$locale().settings.rssFeeds.buttons.copyToClipboard} </button> -Your AniList notifications RSS feed URL +{$locale().settings.rssFeeds.feedUrlLabel} <SettingHint lineBreak> This <a href={'#'} diff --git a/src/lib/Settings/Categories/SettingSync.svelte b/src/lib/Settings/Categories/SettingSync.svelte index 867b2b47..dd19db49 100644 --- a/src/lib/Settings/Categories/SettingSync.svelte +++ b/src/lib/Settings/Categories/SettingSync.svelte @@ -8,6 +8,7 @@ import { addNotification } from "$lib/Notification/store"; import SettingHint from "../SettingHint.svelte"; import locale from "$stores/locale"; import settingsSyncTimes from "$stores/settingsSyncTimes"; +import { get } from "svelte/store"; </script> {#if !$settings.settingsSync} @@ -23,7 +24,7 @@ import settingsSyncTimes from "$stores/settingsSyncTimes"; addNotification( options({ - heading: 'Pulled remote configuration' + heading: get(locale)().notifications?.pulledRemote ?? 'Pulled remote configuration' }) ); } else { @@ -35,7 +36,7 @@ import settingsSyncTimes from "$stores/settingsSyncTimes"; if (response.ok) addNotification( options({ - heading: 'Created remote configuration' + heading: get(locale)().notifications?.createdRemote ?? 'Created remote configuration' }) ); }); @@ -63,8 +64,8 @@ import settingsSyncTimes from "$stores/settingsSyncTimes"; if (response.ok) addNotification( options({ - heading: 'Settings Sync', - description: 'Pushed local configuration to remote' + heading: get(locale)().settings.settingsSync.title, + description: get(locale)().notifications?.pushedRemote ?? 'Pushed local configuration to remote' }) ); }); @@ -82,7 +83,7 @@ import settingsSyncTimes from "$stores/settingsSyncTimes"; addNotification( options({ - heading: 'Settings sync disabled' + heading: get(locale)().notifications?.syncDisabled ?? 'Settings sync disabled' }) ); }} @@ -99,7 +100,7 @@ import settingsSyncTimes from "$stores/settingsSyncTimes"; addNotification( options({ - heading: 'Remote configuration deleted and settings sync disabled' + heading: get(locale)().notifications?.remoteDeleted ?? 'Remote configuration deleted and settings sync disabled' }) ); } @@ -111,7 +112,7 @@ import settingsSyncTimes from "$stores/settingsSyncTimes"; <Spacer /> - <b>Last Push</b>: {$locale().dateFormatter($settingsSyncTimes.lastPush)} + <b>{$locale().settings.settingsSync.lastPush}</b>: {$locale().dateFormatter($settingsSyncTimes.lastPush)} <br /> - <b>Last Pull</b>: {$locale().dateFormatter($settingsSyncTimes.lastPull)} + <b>{$locale().settings.settingsSync.lastPull}</b>: {$locale().dateFormatter($settingsSyncTimes.lastPull)} {/if} diff --git a/src/lib/Settings/SettingCheckboxToggle.svelte b/src/lib/Settings/SettingCheckboxToggle.svelte index 85b4f0cd..1f58520d 100644 --- a/src/lib/Settings/SettingCheckboxToggle.svelte +++ b/src/lib/Settings/SettingCheckboxToggle.svelte @@ -10,7 +10,7 @@ type SettingsBooleanKeys = BooleanSettingsKeys<Settings>; export let sectionBreak = false; export let disabled = false; -export let text: string | (() => string); +export let text: string | undefined | (() => string); export let setting: SettingsBooleanKeys[keyof SettingsBooleanKeys]; export let lineBreak = true; export let onChange: () => void = () => { diff --git a/src/lib/Settings/Verbiage.svelte b/src/lib/Settings/Verbiage.svelte index b82281ee..09caced2 100644 --- a/src/lib/Settings/Verbiage.svelte +++ b/src/lib/Settings/Verbiage.svelte @@ -1,5 +1,6 @@ <script> import root from "$lib/Utility/root"; +import locale from "$stores/locale"; </script> <details open={false}> @@ -7,28 +8,28 @@ import root from "$lib/Utility/root"; <ul> <li> - <a href={root('/')}>Home</a>, Upcoming Episodes: Anime which you have seen all episodes of - thus far, and have a scheduled next episode(s) release date + <a href={root('/')}>{$locale().navigation.home}</a>, {$locale().lists.upcoming.episodes.title}: + {$locale().settings.verbiage?.upcomingEpisodes} </li> <li> - <a href={root('/')}>Home</a>, Not Yet Released: Anime which have not yet aired their first - episode, and have a scheduled next episode(s) release date + <a href={root('/')}>{$locale().navigation.home}</a>, {$locale().lists.upcoming.notYetReleased + .title}: {$locale().settings.verbiage?.notYetReleased} </li> <li> - <a href={root('/')}>Home</a>, Anime: Anime which you have not seen all episodes of thus far, - and have a scheduled next episode release date + <a href={root('/')}>{$locale().navigation.home}</a>, {$locale().settings.media.anime}: + {$locale().settings.verbiage?.dueEpisodes} </li> <li> - <a href={root('/')}>Home</a>, Manga: Manga which you have not read all chapters of thus far, - and have an available next chapter(s) + <a href={root('/')}>{$locale().navigation.home}</a>, {$locale().settings.media.manga}: + {$locale().settings.verbiage?.dueManga} </li> <li> - <a href={root('/completed')}>Completed</a>, Anime: Anime which you have not seen all episodes - of thus far, and have concluded airing + <a href={root('/completed')}>{$locale().navigation.completed}</a>, {$locale().settings.media + .anime}: {$locale().settings.verbiage?.completedAnime} </li> <li> - <a href={root('/completed')}>Completed</a>, Manga: Manga which you have not read all chapters - of thus far, and have concluded publishing + <a href={root('/completed')}>{$locale().navigation.completed}</a>, {$locale().settings.media + .manga}: {$locale().settings.verbiage?.completedManga} </li> </ul> </details> diff --git a/src/lib/Tools/ActivityHistory/Grid.svelte b/src/lib/Tools/ActivityHistory/Grid.svelte index 6a931ab2..afa9cd8f 100644 --- a/src/lib/Tools/ActivityHistory/Grid.svelte +++ b/src/lib/Tools/ActivityHistory/Grid.svelte @@ -11,6 +11,7 @@ import { clearAllParameters } from "../../Utility/parameters"; import Skeleton from "$lib/Loading/Skeleton.svelte"; import tooltip from "$lib/Tooltip/tooltip"; import LogInRestricted from "$lib/Error/LogInRestricted.svelte"; +import locale from "$stores/locale"; export let user: AniListAuthorisation; export let activityData: ActivityHistoryEntry[] | null = null; @@ -52,7 +53,7 @@ const gradientColour = (amount: number, maxAmount: number, baseHue: number) => { role="button" tabindex="0" use:tooltip - title={`Date: ${new Date(activity.date * 1000).toLocaleDateString()}\nAmount: ${ + title={`${$locale().tools.activityHistory?.dateLabel ?? 'Date:'} ${new Date(activity.date * 1000).toLocaleDateString()}\n${$locale().tools.activityHistory?.amountLabel ?? 'Amount:'} ${ activity.amount }`} ></div> diff --git a/src/lib/Tools/ActivityHistory/Tool.svelte b/src/lib/Tools/ActivityHistory/Tool.svelte index 3cf7b09e..5e84db9e 100644 --- a/src/lib/Tools/ActivityHistory/Tool.svelte +++ b/src/lib/Tools/ActivityHistory/Tool.svelte @@ -14,6 +14,7 @@ import ActivityHistoryGrid from "./Grid.svelte"; import SettingHint from "$lib/Settings/SettingHint.svelte"; import Skeleton from "$lib/Loading/Skeleton.svelte"; import LogInRestricted from "$lib/Error/LogInRestricted.svelte"; +import locale from "$stores/locale"; export let user: AniListAuthorisation; @@ -99,10 +100,10 @@ const screenshot = async () => { <Spacer /> <details open> - <summary>Days in risk of developing an activity history hole</summary> + <summary>{$locale().tools.activityHistory?.daysAtRisk}</summary> <SettingHint> - Days in which you did not log any activity or only have one activity logged. + {$locale().tools.activityHistory?.daysAtRiskHint} </SettingHint> <ul> diff --git a/src/lib/Tools/EpisodeDiscussionCollector.svelte b/src/lib/Tools/EpisodeDiscussionCollector.svelte index 68addbdf..2bbefc0a 100644 --- a/src/lib/Tools/EpisodeDiscussionCollector.svelte +++ b/src/lib/Tools/EpisodeDiscussionCollector.svelte @@ -6,6 +6,7 @@ import { clearAllParameters } from "../Utility/parameters"; import Skeleton from "$lib/Loading/Skeleton.svelte"; import InputTemplate from "./InputTemplate.svelte"; import tooltip from "$lib/Tooltip/tooltip"; +import locale from "$stores/locale"; let submission = ""; @@ -46,17 +47,17 @@ onMount(clearAllParameters); {/each} </ol> {:catch} - <p>Threads could not be loaded. You might have been rate-limited.</p> + <p>{$locale().tools.episodeDiscussion?.rateLimit}</p> <p> - Try again in a few minutes. If the problem persists, please contact <a + {$locale().tools.episodeDiscussion?.contactSupport?.split('@fuwn')[0]}<a href="https://anilist.co/user/fuwn" target="_blank">@fuwn</a - > on AniList. + >{$locale().tools.episodeDiscussion?.contactSupport?.split('@fuwn')[1]} </p> {/await} {:else} <Spacer /> - Enter a username to search for to continue. + {$locale().tools.episodeDiscussion?.enterUsername} {/if} </InputTemplate> diff --git a/src/lib/Tools/FollowFix.svelte b/src/lib/Tools/FollowFix.svelte index 6c599569..93d07739 100644 --- a/src/lib/Tools/FollowFix.svelte +++ b/src/lib/Tools/FollowFix.svelte @@ -2,6 +2,7 @@ import { toggleFollow } from "$lib/Data/AniList/follow"; import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; import LogInRestricted from "$lib/Error/LogInRestricted.svelte"; +import locale from "$stores/locale"; export let user: AniListAuthorisation; @@ -28,7 +29,7 @@ let submit = ""; /> {#if input.length > 0} <a href={'#'} onclick={() => (submit = input)}> - Toggle follow for {input} + {$locale({ values: { input } }).tools.followFix?.toggleFor} </a> {/if} </p> diff --git a/src/lib/Tools/InputTemplate.svelte b/src/lib/Tools/InputTemplate.svelte index c90d9b1c..c9d96dfb 100644 --- a/src/lib/Tools/InputTemplate.svelte +++ b/src/lib/Tools/InputTemplate.svelte @@ -3,6 +3,7 @@ import Spacer from "$lib/Layout/Spacer.svelte"; import { clearAllParameters } from "$lib/Utility/parameters"; import { onMount } from "svelte"; import SettingHint from "$lib/Settings/SettingHint.svelte"; +import locale from "$stores/locale"; export let field: string; export let submission: string; @@ -46,7 +47,7 @@ onMount(() => clearAllParameters(saveParameters)); onSubmit(); }} - title="Or click your Enter key" + title={$locale().tools.input?.pressEnter} data-umami-event={event} > {submitText} diff --git a/src/lib/Tools/Likes.svelte b/src/lib/Tools/Likes.svelte index dde5c755..70739ee6 100644 --- a/src/lib/Tools/Likes.svelte +++ b/src/lib/Tools/Likes.svelte @@ -5,6 +5,7 @@ import RateLimited from "$lib/Error/RateLimited.svelte"; import Skeleton from "$lib/Loading/Skeleton.svelte"; import tooltip from "$lib/Tooltip/tooltip"; import settings from "$stores/settings"; +import locale from "$stores/locale"; import InputTemplate from "./InputTemplate.svelte"; let submission = ""; @@ -56,6 +57,6 @@ $: likesPromise = <RateLimited type="Likes" list={false} /> {/await} {:else} - Please enter a valid Activity or Thread URL. + {$locale().tools.likes?.invalidUrl} {/if} </InputTemplate> diff --git a/src/lib/Tools/Picker.svelte b/src/lib/Tools/Picker.svelte index ffece7b6..ad8d3444 100644 --- a/src/lib/Tools/Picker.svelte +++ b/src/lib/Tools/Picker.svelte @@ -2,6 +2,7 @@ import { browser } from "$app/environment"; import { goto } from "$app/navigation"; import root from "$lib/Utility/root"; +import locale from "$stores/locale"; import { tools } from "./tools"; export let tool: string; @@ -14,7 +15,9 @@ export let tool: string; if (browser) goto(root(`/tools/${tool}`)); }} > - <option value="default" selected disabled hidden>Select a tool to continue</option> + <option value="default" selected disabled hidden + >{$locale().tools.picker?.placeholder}</option + > {#each Object.keys(tools).filter((t) => t !== 'default' && !tools[t].hidden) as t} <option value={t}>{tools[t].short || tools[t].name()}</option> diff --git a/src/lib/Tools/SequelCatcher/List.svelte b/src/lib/Tools/SequelCatcher/List.svelte index b1512e22..4b1b8107 100644 --- a/src/lib/Tools/SequelCatcher/List.svelte +++ b/src/lib/Tools/SequelCatcher/List.svelte @@ -4,6 +4,7 @@ import { filterRelations, type Media } from "$lib/Data/AniList/media"; import MediaTitleDisplay from "$lib/List/MediaTitleDisplay.svelte"; import { outboundLink } from "$lib/Media/links"; import settings from "$stores/settings"; +import locale from "$stores/locale"; export let mediaListUnchecked: Media[]; @@ -25,11 +26,11 @@ const matchCheck = (media: Media | undefined, swap = false) => : undefined; </script> -<input type="checkbox" bind:checked={includeCurrent} /> Include current (watching, rewatching, -paused) +<input type="checkbox" bind:checked={includeCurrent} /> +{$locale().tools.sequelCatcher?.includeCurrent} <br /> -<input type="checkbox" bind:checked={includeSideStories} /> Include side stories (e.g., OVAs, -specials, etc.) +<input type="checkbox" bind:checked={includeSideStories} /> +{$locale().tools.sequelCatcher?.includeSideStories} <Spacer /> diff --git a/src/lib/Tools/SequelCatcher/Tool.svelte b/src/lib/Tools/SequelCatcher/Tool.svelte index 727a3a6c..f75b1f78 100644 --- a/src/lib/Tools/SequelCatcher/Tool.svelte +++ b/src/lib/Tools/SequelCatcher/Tool.svelte @@ -12,6 +12,7 @@ import lastPruneTimes from "$stores/lastPruneTimes"; import Message from "$lib/Loading/Message.svelte"; import Skeleton from "$lib/Loading/Skeleton.svelte"; import Username from "$lib/Layout/Username.svelte"; +import locale from "$stores/locale"; export let user: AniListAuthorisation; @@ -69,13 +70,19 @@ onMount(async () => { /> {/if} {:catch} - <Message message="" loader="ripple" slot withReload fullscreen>Error fetching media.</Message> + <Message message="" loader="ripple" slot withReload fullscreen + >{$locale().tools.wrapped?.errorFetchingMedia ?? 'Error fetching media.'}</Message + > {/await} <Spacer /> <blockquote style="margin: 0 0 0 1.5rem;"> - Thanks to <Username username="sevengirl" /> and <Username username="esthereae" /> for the idea! + {$locale().tools.sequelCatcher?.credit?.split('@sevengirl')[0]}<Username + username="sevengirl" + />{$locale().tools.sequelCatcher?.credit?.split('@sevengirl')[1]?.split('@esthereae')[0]}<Username + username="esthereae" + />{$locale().tools.sequelCatcher?.credit?.split('@esthereae')[1]} </blockquote> </div> {/if} diff --git a/src/lib/Tools/SequelSpy/Tool.svelte b/src/lib/Tools/SequelSpy/Tool.svelte index 71056694..87931176 100644 --- a/src/lib/Tools/SequelSpy/Tool.svelte +++ b/src/lib/Tools/SequelSpy/Tool.svelte @@ -10,6 +10,7 @@ import { season as getSeason } from "$lib/Media/Anime/season"; import Skeleton from "$lib/Loading/Skeleton.svelte"; import identity from "$stores/identity"; import LogInRestricted from "$lib/Error/LogInRestricted.svelte"; +import locale from "$stores/locale"; import Prequels from "./Prequels.svelte"; export let user: AniListAuthorisation; @@ -45,10 +46,10 @@ onMount(() => clearAllParameters(["year", "season"])); <div class="card"> <p> <select bind:value={season}> - <option value="WINTER">Winter</option> - <option value="SPRING">Spring</option> - <option value="SUMMER">Summer</option> - <option value="FALL">Fall</option> + <option value="WINTER">{$locale().tools.sequelSpy?.winter}</option> + <option value="SPRING">{$locale().tools.sequelSpy?.spring}</option> + <option value="SUMMER">{$locale().tools.sequelSpy?.summer}</option> + <option value="FALL">{$locale().tools.sequelSpy?.fall}</option> </select> <input type="number" bind:value={year} /> </p> @@ -61,7 +62,6 @@ onMount(() => clearAllParameters(["year", "season"])); <Spacer /> - The count ratio is the number of episodes you've seen of any direct prequels, and the total - number of episodes of all direct prequels. + {$locale().tools.sequelSpy?.countRatio} </div> {/if} diff --git a/src/lib/Tools/Tracker/Tool.svelte b/src/lib/Tools/Tracker/Tool.svelte index 8f7a3197..b495522a 100644 --- a/src/lib/Tools/Tracker/Tool.svelte +++ b/src/lib/Tools/Tracker/Tool.svelte @@ -4,6 +4,8 @@ import { v6 as uuidv6 } from "uuid"; import { database, type TrackerEntry } from "$lib/Database/IDB/tracker"; import { onMount } from "svelte"; import Message from "$lib/Loading/Message.svelte"; +import locale from "$stores/locale"; +import { get } from "svelte/store"; let url = ""; let title = ""; @@ -34,15 +36,19 @@ const adjustEntry = (id: string, to: number) => { const addEntry = async (url: string, title: string, progress: number) => { if (!url || !title) { - error = "URL and title are required fields"; + error = + get(locale)().tools.tracker?.urlTitleRequired ?? + "URL and title are required fields"; return; } if (listAccess.some((entry) => entry.url === url)) { - error = - "Entry with URL already exists: " + - listAccess.find((entry) => entry.url === url)?.title; + const existing = listAccess.find((entry) => entry.url === url)?.title; + + error = ( + get(locale)().tools.tracker?.entryExists ?? "Entry with URL already exists: {url}" + ).replace("{url}", existing ?? ""); return; } @@ -55,7 +61,9 @@ const addEntry = async (url: string, title: string, progress: number) => { const deleteEntry = async (id: string) => { if (confirmDelete !== 1) { confirmDelete = 1; - error = "Click again to confirm deletion"; + error = + get(locale)().tools.tracker?.confirmDelete ?? + "Click again to confirm deletion"; return; } @@ -73,10 +81,16 @@ const deleteEntry = async (id: string) => { <p><b>Error</b>: {error}</p> {/if} - <input type="url" placeholder="URL" bind:value={url} /> - <input type="text" placeholder="Title" bind:value={title} /> - <input type="number" placeholder="Progress (defaults to 0)" bind:value={progress} /> - <button class="button-lined" onclick={() => addEntry(url, title, progress)}> Add </button> + <input type="url" placeholder={$locale().tools.tracker?.urlPlaceholder} bind:value={url} /> + <input type="text" placeholder={$locale().tools.tracker?.titlePlaceholder} bind:value={title} /> + <input + type="number" + placeholder={$locale().tools.tracker?.progressPlaceholder} + bind:value={progress} + /> + <button class="button-lined" onclick={() => addEntry(url, title, progress)} + >{$locale().common?.add}</button + > <Spacer /> @@ -120,7 +134,7 @@ const deleteEntry = async (id: string) => { + </button> <span class="opaque">|</span> - <button onclick={() => deleteEntry(entry.id)}>Remove</button> + <button onclick={() => deleteEntry(entry.id)}>{$locale().common?.remove}</button> </span> </div> </li> diff --git a/src/lib/Tools/Wrapped/Tool.svelte b/src/lib/Tools/Wrapped/Tool.svelte index b04bc5f6..b7d67cb0 100644 --- a/src/lib/Tools/Wrapped/Tool.svelte +++ b/src/lib/Tools/Wrapped/Tool.svelte @@ -2,6 +2,7 @@ import Spacer from "$lib/Layout/Spacer.svelte"; import "./wrapped.css"; import userIdentity from "$stores/identity"; +import locale from "$stores/locale"; import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; import { onMount } from "svelte"; import { @@ -214,33 +215,36 @@ $: { updateWidth(); } $: genreTagTitle = (() => { + const w = $locale().tools.wrapped; switch (genreTagsSort) { case SortOptions.SCORE: - return "Highest Rated"; + return w?.highestRated ?? "Highest Rated"; case SortOptions.MINUTES_WATCHED: - return "Most Watched"; + return w?.mostWatched ?? "Most Watched"; case SortOptions.COUNT: - return "Most Common"; + return w?.mostCommon ?? "Most Common"; } })(); $: animeMostTitle = (() => { + const w = $locale().tools.wrapped; switch (mediaSort) { case SortOptions.SCORE: - return "Highest Rated"; + return w?.highestRated ?? "Highest Rated"; case SortOptions.MINUTES_WATCHED: - return "Most Watched"; + return w?.mostWatched ?? "Most Watched"; case SortOptions.COUNT: - return "Most Common"; + return w?.mostCommon ?? "Most Common"; } })(); $: mangaMostTitle = (() => { + const w = $locale().tools.wrapped; switch (mediaSort) { case SortOptions.SCORE: - return "Highest Rated"; + return w?.highestRated ?? "Highest Rated"; case SortOptions.MINUTES_WATCHED: - return "Most Read"; + return w?.mostRead ?? "Most Read"; case SortOptions.COUNT: - return "Most Common"; + return w?.mostCommon ?? "Most Common"; } })(); @@ -829,12 +833,12 @@ const pruneFullYear = async () => { {#if shouldFetchData} {#key fetchKey} {#await selectedYear !== currentYear || useFullActivityHistory || new Date().getMonth() <= 6 ? fullActivityHistory(user, $userIdentity, selectedYear, disableLoopingActivityCounter) : getActivityHistory($userIdentity)} - <Message message="Loading activity history ..." /> + <Message message={$locale().tools.wrapped?.loadingActivityHistory} /> <Skeleton count={2} /> {:then activities} {#await wrapped(user, $userIdentity, selectedYear, false, disableLoopingActivityCounter)} - <Message message="Loading user data ..." /> + <Message message={$locale().tools.wrapped?.loadingUserData} /> <Skeleton count={2} /> {:then wrapped} @@ -920,9 +924,8 @@ const pruneFullYear = async () => { > {#if useFullActivityHistory} <p> - With <b>many</b> activities, it may take multiple attempts to obtain all of your activity - history from AniList. If this occurs, wait one minute and try again to continue populating - your local activity history database. + {$locale().tools.wrapped?.multiAttemptPrefix}<b>many</b>{$locale().tools.wrapped + ?.multiAttemptSuffix} </p> {/if} </RateLimitedError> @@ -970,7 +973,7 @@ const pruneFullYear = async () => { id="watermark" data-umami-event="Load Wrapped Data" > - Click load data! + {$locale().tools.wrapped?.clickLoadData} </a> </div> </div> @@ -985,7 +988,7 @@ const pruneFullYear = async () => { <Spacer /> <blockquote style="margin: 0 0 0 1.5rem;"> - Click on the image to download, or right click and select "Save Image As...". + {$locale().tools.wrapped?.saveImageInstruction} </blockquote> {/if} </div> @@ -995,66 +998,73 @@ const pruneFullYear = async () => { {/if} <div id="options" class="card"> - <button onclick={screenshot} data-umami-event="Generate Wrapped"> Generate image </button> + <button onclick={screenshot} data-umami-event="Generate Wrapped" + >{$locale().tools.wrapped?.generateImage}</button + > {#if !shouldFetchData} - <button onclick={triggerFetch} data-umami-event="Load Wrapped Data"> Load data </button> + <button onclick={triggerFetch} data-umami-event="Load Wrapped Data" + >{$locale().tools.wrapped?.loadData}</button + > {:else if needsRefetch} <button onclick={triggerFetch} data-umami-event="Refetch Wrapped Data"> - Reload data + {$locale().tools.wrapped?.reloadData} </button> {/if} <details class="no-shadow" open> - <summary>Display</summary> + <summary>{$locale().tools.wrapped?.display}</summary> - <input type="checkbox" bind:checked={watermark} /> Show watermark<br /> - <input type="checkbox" bind:checked={transparency} /> Enable background transparency<br /> + <input type="checkbox" bind:checked={watermark} /> + {$locale().tools.wrapped?.showWatermark}<br /> + <input type="checkbox" bind:checked={transparency} /> + {$locale().tools.wrapped?.bgTransparency}<br /> <input type="checkbox" bind:checked={lightMode} /> - Enable light mode<br /> + {$locale().tools.wrapped?.lightMode}<br /> <input type="checkbox" bind:checked={topGenresTags} /> - Show top genres and tags<br /> + {$locale().tools.wrapped?.showGenresTags}<br /> <input type="checkbox" bind:checked={disableActivityHistory} disabled={selectedYear !== currentYear} /> - Hide activity history<br /> - <input type="checkbox" bind:checked={highestRatedMediaPercentage} /> Show highest rated - media percentages<br /> - <input type="checkbox" bind:checked={highestRatedGenreTagPercentage} /> Show highest rated - genre and tag percentages<br /> - <input type="checkbox" bind:checked={includeOngoingMediaFromPreviousYears} /> Show ongoing - media from previous years<br /> + {$locale().tools.wrapped?.hideActivityHistory}<br /> + <input type="checkbox" bind:checked={highestRatedMediaPercentage} /> + {$locale().tools.wrapped?.showRatedPercentages}<br /> + <input type="checkbox" bind:checked={highestRatedGenreTagPercentage} /> + {$locale().tools.wrapped?.showGenreTagPercentages}<br /> + <input type="checkbox" bind:checked={includeOngoingMediaFromPreviousYears} /> + {$locale().tools.wrapped?.showOngoingPrevious}<br /> <select bind:value={activityHistoryPosition}> - <option value="TOP">Above Top Row</option> - <option value="BELOW_TOP">Below Top Row</option> - <option value="ORIGINAL">Bottom</option> + <option value="TOP">{$locale().tools.wrapped?.aboveTopRow}</option> + <option value="BELOW_TOP">{$locale().tools.wrapped?.belowTopRow}</option> + <option value="ORIGINAL">{$locale().tools.wrapped?.bottom}</option> </select> - Activity history position<br /> + {$locale().tools.wrapped?.activityHistoryPosition}<br /> <select bind:value={highestRatedCount}> {#each [3, 4, 5, 6, 7, 8, 9, 10] as count} <option value={count}>{count}</option> {/each} </select> - Highest rated media count<br /> + {$locale().tools.wrapped?.highestRatedCount}<br /> <select bind:value={genreTagCount}> {#each [3, 4, 5, 6, 7, 8, 9, 10] as count} <option value={count}>{count}</option> {/each} </select> - Highest genre and tag count<br /> - <button onclick={updateWidth}>Find best fit</button> + {$locale().tools.wrapped?.highestGenreTagCount}<br /> + <button onclick={updateWidth}>{$locale().tools.wrapped?.findBestFit}</button> <button onclick={() => (width -= 25)}>-25px</button> <button onclick={() => (width += 25)}>+25px</button> - Width adjustment<br /> + {$locale().tools.wrapped?.widthAdjustment}<br /> </details> <details class="no-shadow" open> - <summary>Calculation</summary> + <summary>{$locale().tools.wrapped?.calculation}</summary> <input type="checkbox" bind:checked={useFullActivityHistory} disabled={needsRefetch} /> - Enable full-year activity<button class="smaller-button" onclick={pruneFullYear} - >Refresh data</button + {$locale().tools.wrapped?.enableFullYear}<button + class="smaller-button" + onclick={pruneFullYear}>{$locale().tools.wrapped?.refreshData}</button > <br /> <select bind:value={selectedYear} disabled={needsRefetch}> @@ -1064,7 +1074,7 @@ const pruneFullYear = async () => { </option> {/each} </select> - Calculate for year<br /> + {$locale().tools.wrapped?.calculateForYear}<br /> <input type="date" bind:value={startDateFilter} @@ -1076,7 +1086,7 @@ const pruneFullYear = async () => { update(); }} /> - Start date filter<br /> + {$locale().tools.wrapped?.startDateFilter}<br /> <input type="date" bind:value={endDateFilter} @@ -1088,25 +1098,30 @@ const pruneFullYear = async () => { update(); }} /> - End date filter<br /> + {$locale().tools.wrapped?.endDateFilter}<br /> <select bind:value={mediaSort}> <option value={SortOptions.SCORE}>Score</option> <option value={SortOptions.MINUTES_WATCHED}>Minutes Watched/Read</option> </select> - Anime and manga sort<br /> + {$locale().tools.wrapped?.animeMangaSort}<br /> <select bind:value={genreTagsSort}> <option value={SortOptions.SCORE}>Score</option> <option value={SortOptions.MINUTES_WATCHED}>Minutes Watched/Read</option> <option value={SortOptions.COUNT}>Count</option> </select> - Genre and tag sort<br /> - <input type="checkbox" bind:checked={includeMusic} /> Include music<br /> - <input type="checkbox" bind:checked={includeRepeats} /> Include rewatches & rereads<br /> - <input type="checkbox" bind:checked={includeSpecials} /> Include specials<br /> - <input type="checkbox" bind:checked={includeOVAs} /> Include OVAs<br /> - <input type="checkbox" bind:checked={includeMovies} /> Include movies<br /> - <input type="checkbox" bind:checked={excludeUnratedUnwatched} /> Excluded unrated & - unwatched<br /> + {$locale().tools.wrapped?.genreTagSort}<br /> + <input type="checkbox" bind:checked={includeMusic} /> + {$locale().tools.wrapped?.includeMusic}<br /> + <input type="checkbox" bind:checked={includeRepeats} /> + {$locale().tools.wrapped?.includeRewatches}<br /> + <input type="checkbox" bind:checked={includeSpecials} /> + {$locale().tools.wrapped?.includeSpecials}<br /> + <input type="checkbox" bind:checked={includeOVAs} /> + {$locale().tools.wrapped?.includeOvas}<br /> + <input type="checkbox" bind:checked={includeMovies} /> + {$locale().tools.wrapped?.includeMovies}<br /> + <input type="checkbox" bind:checked={excludeUnratedUnwatched} /> + {$locale().tools.wrapped?.excludeUnrated}<br /> <input type="text" bind:value={excludedKeywordsInput} @@ -1114,29 +1129,33 @@ const pruneFullYear = async () => { e.key === 'Enter' && submitExcludedKeywords(); }} /> - Excluded keywords - <button onclick={submitExcludedKeywords} title="Or click your Enter key" use:tooltip> - Submit + {$locale().tools.wrapped?.excludedKeywords} + <button + onclick={submitExcludedKeywords} + title={$locale().tools.input?.pressEnter} + use:tooltip + > + {$locale().tools.wrapped?.submit} </button> <br /> - <SettingHint>Comma separated list (e.g., "My Hero, Kaguya")</SettingHint> + <SettingHint>{$locale().tools.wrapped?.excludedHint}</SettingHint> </details> <details class="no-shadow" open> - <summary>Advanced</summary> + <summary>{$locale().tools.wrapped?.advanced}</summary> <input type="checkbox" bind:checked={disableLoopingActivityCounter} disabled={needsRefetch} /> - Disable detailed activity information + {$locale().tools.wrapped?.disableDetailedActivity} </details> </div> </div> </div> {:else} - <Message message="Loading user ..." /> + <Message message={$locale().tools.wrapped?.loadingUser} /> <Skeleton count={2} /> {/if} diff --git a/src/lib/User/BadgeWall/AWC.svelte b/src/lib/User/BadgeWall/AWC.svelte index da01e484..5684eb2f 100644 --- a/src/lib/User/BadgeWall/AWC.svelte +++ b/src/lib/User/BadgeWall/AWC.svelte @@ -4,6 +4,7 @@ import type { AWCBadgesGroup } from "$lib/Data/awc"; import { cdn, thumbnail } from "$lib/Utility/image"; import type { Preferences } from "../../../graphql/$types"; import FallbackBadge from "./FallbackBadge.svelte"; +import locale from "$stores/locale"; import "./badges.css"; export let awcPromise: Promise<Response>; @@ -79,7 +80,8 @@ const awcBadgesGrouped = (awcResponse: string): AWCBadgesGroup[] => { {#each parsedBadges as group} <details open={categoryFilter || isOwner ? false : true}> <summary> - Anime Watching Club <span class="opaque">|</span> + {$locale().badgeWall?.awcGroup} + <span class="opaque">|</span> {group.group} </summary> diff --git a/src/lib/User/BadgeWall/BadgePreview.svelte b/src/lib/User/BadgeWall/BadgePreview.svelte index 104a53da..ebf47d9b 100644 --- a/src/lib/User/BadgeWall/BadgePreview.svelte +++ b/src/lib/User/BadgeWall/BadgePreview.svelte @@ -147,7 +147,7 @@ const onClick = (event: MouseEvent) => { {/if} {#if selectedBadge.designer} - <b>Designer:</b> + <b>{$locale().badgePreview?.designer}</b> <!-- {#if selectedBadge.designer.startsWith('http')} <a href={selectedBadge.designer} target="_blank"> @@ -166,7 +166,11 @@ const onClick = (event: MouseEvent) => { {/if} {#if selectedBadge.post && selectedBadge.post !== '#'} - <b>{selectedBadge.post.includes('forum') ? 'Forum' : 'Activity'}:</b> + <b + >{selectedBadge.post.includes('forum') + ? $locale().badgePreview?.forum + : $locale().badgePreview?.activity}:</b + > <a href={selectedBadge.post} target="_blank"> {selectedBadge.post} @@ -176,7 +180,7 @@ const onClick = (event: MouseEvent) => { {/if} {#if selectedBadge.source} - <b>Source:</b> + <b>{$locale().badgePreview?.source}</b> {#if selectedBadge.source.startsWith('http')} <!-- <a href={selectedBadge.source} target="_blank"> @@ -191,7 +195,7 @@ const onClick = (event: MouseEvent) => { {/if} {#if selectedBadge.category} - <b>Category:</b> + <b>{$locale().badgePreview?.category}</b> <a href={`?category=${selectedBadge.category}`} onclick={() => (selectedBadge = undefined)}> {selectedBadge.category} @@ -200,18 +204,18 @@ const onClick = (event: MouseEvent) => { <br /> {/if} - <b>SauceNAO:</b> + <b>{$locale().badgePreview?.sauceNAO}</b> <a href={`https://saucenao.com/search.php?url=${selectedBadge.image}`} target="_blank"> - Search + {$locale().badgePreview?.search} </a> <div class="badge-preview-seek"> {#if hasPrevious} - <button onclick={onPrevious}>Previous</button> + <button onclick={onPrevious}>{$locale().badgePreview?.previous}</button> {/if} {#if hasNext} - <button onclick={onNext} style="float: right;">Next</button> + <button onclick={onNext} style="float: right;">{$locale().badgePreview?.next}</button> {/if} </div> </div> diff --git a/src/lib/User/BadgeWall/Badges.svelte b/src/lib/User/BadgeWall/Badges.svelte index 67c00c53..1d620c2f 100644 --- a/src/lib/User/BadgeWall/Badges.svelte +++ b/src/lib/User/BadgeWall/Badges.svelte @@ -20,7 +20,8 @@ export let selectedBadge: IndexedBadge | undefined = undefined; {#if ungroupedBadges.length === 0} <div class="card"> - No due.moe registered badges found for this user. <a + {$locale().badgeWall?.noRegistered} + <a href={'#'} onclick={(e) => e.preventDefault()} title="This alert does not include AWC badges." diff --git a/src/lib/User/BadgeWall/FallbackBadge.svelte b/src/lib/User/BadgeWall/FallbackBadge.svelte index a930e5e0..04793aa0 100644 --- a/src/lib/User/BadgeWall/FallbackBadge.svelte +++ b/src/lib/User/BadgeWall/FallbackBadge.svelte @@ -113,7 +113,12 @@ const asAWCBadge = (b: Badge | AWCBadge) => b as AWCBadge; </a> </Tooltip> {:else if !hideOnError} - <img src={error} alt="Not found" loading="lazy" class="badge" /> + <img + src={error} + alt={$locale().badgeWall?.notFound ?? 'Not found'} + loading="lazy" + class="badge" + /> {/if} <style lang="scss"> diff --git a/src/lib/Utility/Loading.svelte b/src/lib/Utility/Loading.svelte index 67691cbb..a84570b7 100644 --- a/src/lib/Utility/Loading.svelte +++ b/src/lib/Utility/Loading.svelte @@ -1,4 +1,6 @@ <script lang="ts"> +import locale from "$stores/locale"; + export let type: string | undefined = undefined; export let percent: number | undefined = undefined; export let card = true; @@ -6,7 +8,9 @@ export let card = true; <div class:card> {#if type} - Loading {type} ...{percent ? ` ${percent}%` : ''} + {$locale({ + values: { type, percent: percent ? ` ${percent}%` : '' } + }).common?.loading ?? `Loading ${type} ...${percent ? ` ${percent}%` : ''}`} {:else} <slot /> {/if} |