aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/CommandPalette/CommandPalette.svelte16
-rw-r--r--src/lib/CommandPalette/actions.ts356
-rw-r--r--src/lib/CommandPalette/authActions.ts8
-rw-r--r--src/lib/CommandPalette/syncActions.ts35
-rw-r--r--src/lib/CommandPalette/toggleActions.ts56
-rw-r--r--src/lib/Data/AniList/media.ts6
-rw-r--r--src/lib/Error/AnimeRateLimited.svelte3
-rw-r--r--src/lib/Error/LogInRestricted.svelte7
-rw-r--r--src/lib/Error/RateLimited.svelte31
-rw-r--r--src/lib/Hololive/Lives.svelte3
-rw-r--r--src/lib/Home/HeadTitle.svelte21
-rw-r--r--src/lib/Home/LastActivity.svelte10
-rw-r--r--src/lib/Image/FallbackImage.svelte4
-rw-r--r--src/lib/List/Anime/CleanAnimeList.svelte10
-rw-r--r--src/lib/List/Manga/CleanMangaList.svelte36
-rw-r--r--src/lib/List/Manga/MangaListTemplate.svelte7
-rw-r--r--src/lib/List/MediaRoulette.svelte34
-rw-r--r--src/lib/Loading/Message.svelte5
-rw-r--r--src/lib/Locale/english.ts485
-rw-r--r--src/lib/Locale/japanese.ts4
-rw-r--r--src/lib/Locale/layout.ts429
-rw-r--r--src/lib/Media/invalidate.ts6
-rw-r--r--src/lib/Reader/Chapters/MangaDex.svelte8
-rw-r--r--src/lib/Schedule/Crunchyroll.svelte5
-rw-r--r--src/lib/Settings/Categories/Attributions.svelte29
-rw-r--r--src/lib/Settings/Categories/Cache.svelte12
-rw-r--r--src/lib/Settings/Categories/Calculation.svelte35
-rw-r--r--src/lib/Settings/Categories/Debug.svelte9
-rw-r--r--src/lib/Settings/Categories/Display.svelte77
-rw-r--r--src/lib/Settings/Categories/RSSFeeds.svelte5
-rw-r--r--src/lib/Settings/Categories/SettingSync.svelte17
-rw-r--r--src/lib/Settings/SettingCheckboxToggle.svelte2
-rw-r--r--src/lib/Settings/Verbiage.svelte25
-rw-r--r--src/lib/Tools/ActivityHistory/Grid.svelte3
-rw-r--r--src/lib/Tools/ActivityHistory/Tool.svelte5
-rw-r--r--src/lib/Tools/EpisodeDiscussionCollector.svelte9
-rw-r--r--src/lib/Tools/FollowFix.svelte3
-rw-r--r--src/lib/Tools/InputTemplate.svelte3
-rw-r--r--src/lib/Tools/Likes.svelte3
-rw-r--r--src/lib/Tools/Picker.svelte5
-rw-r--r--src/lib/Tools/SequelCatcher/List.svelte9
-rw-r--r--src/lib/Tools/SequelCatcher/Tool.svelte11
-rw-r--r--src/lib/Tools/SequelSpy/Tool.svelte12
-rw-r--r--src/lib/Tools/Tracker/Tool.svelte34
-rw-r--r--src/lib/Tools/Wrapped/Tool.svelte141
-rw-r--r--src/lib/User/BadgeWall/AWC.svelte4
-rw-r--r--src/lib/User/BadgeWall/BadgePreview.svelte20
-rw-r--r--src/lib/User/BadgeWall/Badges.svelte3
-rw-r--r--src/lib/User/BadgeWall/FallbackBadge.svelte7
-rw-r--r--src/lib/Utility/Loading.svelte6
-rw-r--r--src/routes/+error.svelte5
-rw-r--r--src/routes/+layout.svelte10
-rw-r--r--src/routes/completed/+page.svelte4
-rw-r--r--src/routes/events/+page.svelte7
-rw-r--r--src/routes/events/group/[group]/+page.svelte23
-rw-r--r--src/routes/events/groups/+page.svelte11
-rw-r--r--src/routes/girls/+page.svelte13
-rw-r--r--src/routes/girls/[language]/+page.svelte5
-rw-r--r--src/routes/hololive/[[stream]]/+page.svelte14
-rw-r--r--src/routes/reader/+page.svelte16
-rw-r--r--src/routes/schedule/+page.svelte9
-rw-r--r--src/routes/settings/+page.svelte30
-rw-r--r--src/routes/tools/+page.svelte7
-rw-r--r--src/routes/tools/[tool]/+page.svelte7
-rw-r--r--src/routes/updates/+page.svelte11
-rw-r--r--src/routes/user/+page.svelte2
-rw-r--r--src/routes/user/[user]/+page.svelte45
-rw-r--r--src/routes/user/[user]/badges/+page.svelte103
68 files changed, 1770 insertions, 626 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">&times;</button>
+ <button class="close-button" onclick={handleClose} aria-label={$locale().lists.roulette?.closeAriaLabel}
+ >&times;</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}
diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte
index 71482ffb..a67cdbbf 100644
--- a/src/routes/+error.svelte
+++ b/src/routes/+error.svelte
@@ -2,6 +2,7 @@
import { page } from "$app/stores";
import { closest } from "$lib/Error/path";
import Popup from "$lib/Layout/Popup.svelte";
+import locale from "$stores/locale";
$: suggestion = closest($page.url.pathname.replace("/", ""), [
"birthdays",
@@ -18,11 +19,11 @@ $: suggestion = closest($page.url.pathname.replace("/", ""), [
<Popup>
<p style="text-align: center;">
- <a href={$page.url.pathname}>{$page.url.pathname}</a> not found
+ <a href={$page.url.pathname}>{$page.url.pathname}</a> {$locale().errors?.routeNotFound}
</p>
<blockquote style="margin: 0 0 0 1.5rem;">
- Did you mean "<a
+ {$locale().errors?.didYouMean} "<a
href={suggestion}
style={suggestion === '...' ? 'pointer-events: none; color: inherit;' : ''}
>{suggestion.charAt(0).toUpperCase() + suggestion.slice(1)}</a
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 71d74adb..b1f9fc87 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -325,7 +325,7 @@ $: {
<CommandPalette
items={[
- ...defaultActions,
+ ...defaultActions(),
...toolsAsCommandPaletteActions(),
...authActions(data.user),
...syncActions($userIdentity.id, $settings.settingsSync),
@@ -356,7 +356,7 @@ $: {
<button
type="button"
class="menu-toggle"
- aria-label="Menu"
+ aria-label={$locale().navigation.menu}
aria-expanded={isMenuOpen}
aria-controls="primary-nav"
onclick={() => (isMenuOpen = !isMenuOpen)}
@@ -434,7 +434,11 @@ $: {
</a>
{:else if data.user}
<a href={root(`/user/${$userIdentity.name}`)} class="header-item">
- <img class="avatar" src={$userIdentity.avatar} alt="Avatar" />
+ <img
+ class="avatar"
+ src={$userIdentity.avatar}
+ alt={$locale().navigation.avatar}
+ />
</a>
{/if}
</div>
diff --git a/src/routes/completed/+page.svelte b/src/routes/completed/+page.svelte
index bcad912b..3720bf60 100644
--- a/src/routes/completed/+page.svelte
+++ b/src/routes/completed/+page.svelte
@@ -64,14 +64,14 @@ onMount(async () => {
onDestroy(() => removeHeightObserver?.());
</script>
-<HeadTitle route="Completed" path="/completed" />
+<HeadTitle routeKey="completed" path="/completed" />
{#if LastActivityComponent}
<LastActivityComponent user={data.user} />
{/if}
{#if data.user === undefined}
- <div class="card">Please log in to view completed media.</div>
+ <div class="card">{$locale().errors?.completedLoginPrompt}</div>
<Spacer />
diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte
index 88a3da9e..0474852b 100644
--- a/src/routes/events/+page.svelte
+++ b/src/routes/events/+page.svelte
@@ -4,13 +4,14 @@ import Event from "$lib/Events/Event.svelte";
import Message from "$lib/Loading/Message.svelte";
import root from "$lib/Utility/root";
+import locale from "$stores/locale";
</script>
{#await fetch(root(`/api/events`))}
- <Message message="Loading events ..." />
+ <Message message={$locale().events?.loadingEvents} />
{:then eventsResponse}
{#await eventsResponse.json()}
- <Message message="Parsing events ..." />
+ <Message message={$locale().events?.parsingEvents} />
{:then events}
{#if events}
{#each events as rawEvent, i}
@@ -22,6 +23,6 @@ import root from "$lib/Utility/root";
{/each}
{/if}
{:catch}
- <Message message="" loader="ripple" slot withReload>Error parsing events.</Message>
+ <Message message="" loader="ripple" slot withReload>{$locale().events?.errorParsingEvents}</Message>
{/await}
{/await}
diff --git a/src/routes/events/group/[group]/+page.svelte b/src/routes/events/group/[group]/+page.svelte
index c02c1b51..34db0fae 100644
--- a/src/routes/events/group/[group]/+page.svelte
+++ b/src/routes/events/group/[group]/+page.svelte
@@ -7,6 +7,7 @@ import root from "$lib/Utility/root";
import { onMount } from "svelte";
import Group from "$lib/Events/Group.svelte";
import Event from "$lib/Events/Event.svelte";
+import locale from "$stores/locale";
import type { PageData } from "./$types";
export let data: PageData;
@@ -23,16 +24,16 @@ const asEvent = (event: unknown) => event as EventType;
</script>
{#await groupsResponse}
- <Message message="Loading group ..." />
+ <Message message={$locale().events?.loadingGroup} />
{:then group}
{#if group}
{#await group.json()}
- <Message message="Parsing group ..." />
+ <Message message={$locale().events?.parsingGroup} />
{:then json}
{#if json === null}
<Message message="" loader="ripple" slot>
- This group may not exist. Please
- <a href={'#'} onclick={() => location.reload()}>try again</a> later.
+ {$locale().events?.groupNotExistPrefix}
+ <a href={'#'} onclick={() => location.reload()}>{$locale().common?.tryAgain}</a>{$locale().events?.groupNotExistSuffix}
</Message>
{:else}
{@const group = asGroup(json)}
@@ -42,13 +43,13 @@ const asEvent = (event: unknown) => event as EventType;
<Spacer />
<details open>
- <summary>Events</summary>
+ <summary>{$locale().events?.summary}</summary>
{#await fetch(root(`/api/events?group=${data.group}`))}
- <Message message="Loading events ..." />
+ <Message message={$locale().events?.loadingEvents} />
{:then eventsResponse}
{#await eventsResponse.json()}
- <Message message="Parsing events ..." />
+ <Message message={$locale().events?.parsingEvents} />
{:then events}
{#if events}
{#each events as rawEvent, i}
@@ -60,17 +61,17 @@ const asEvent = (event: unknown) => event as EventType;
{/each}
{/if}
{:catch}
- <Message message="" loader="ripple" slot withReload>Error parsing events.</Message>
+ <Message message="" loader="ripple" slot withReload>{$locale().events?.errorParsingEvents}</Message>
{/await}
{/await}
</details>
{/if}
{:catch}
- <Message message="" loader="ripple" slot withReload>Error parsing group.</Message>
+ <Message message="" loader="ripple" slot withReload>{$locale().events?.errorParsingGroup}</Message>
{/await}
{:else}
- <Message message="Parsing groups ..." />
+ <Message message={$locale().events?.parsingGroups} />
{/if}
{:catch}
- <Message message="" loader="ripple" slot withReload>Error loading group.</Message>
+ <Message message="" loader="ripple" slot withReload>{$locale().events?.errorLoadingGroup}</Message>
{/await}
diff --git a/src/routes/events/groups/+page.svelte b/src/routes/events/groups/+page.svelte
index b6181ad8..198c637b 100644
--- a/src/routes/events/groups/+page.svelte
+++ b/src/routes/events/groups/+page.svelte
@@ -5,6 +5,7 @@ import Message from "$lib/Loading/Message.svelte";
import root from "$lib/Utility/root";
import { onMount } from "svelte";
import Group from "$lib/Events/Group.svelte";
+import locale from "$stores/locale";
let groupsResponse: Promise<Response>;
@@ -16,11 +17,11 @@ const asGroup = (group: unknown) => group as GroupType;
</script>
{#await groupsResponse}
- <Message message="Loading groups ..." />
+ <Message message={$locale().events?.loadingGroups} />
{:then groups}
{#if groups}
{#await groups.json()}
- <Message message="Parsing groups ..." />
+ <Message message={$locale().events?.parsingGroups} />
{:then json}
{#each json as rawGroup, i}
{@const group = asGroup(rawGroup)}
@@ -34,11 +35,11 @@ const asGroup = (group: unknown) => group as GroupType;
{/if}
{/each}
{:catch}
- <Message message="" loader="ripple" slot withReload>Error parsing groups.</Message>
+ <Message message="" loader="ripple" slot withReload>{$locale().events?.errorParsingGroups}</Message>
{/await}
{:else}
- <Message message="Parsing groups ..." />
+ <Message message={$locale().events?.parsingGroups} />
{/if}
{:catch}
- <Message message="" loader="ripple" slot withReload>Error loading groups.</Message>
+ <Message message="" loader="ripple" slot withReload>{$locale().events?.errorLoadingGroups}</Message>
{/await}
diff --git a/src/routes/girls/+page.svelte b/src/routes/girls/+page.svelte
index bbe57ac9..e45dad4d 100644
--- a/src/routes/girls/+page.svelte
+++ b/src/routes/girls/+page.svelte
@@ -5,28 +5,29 @@ import HeadTitle from "$lib/Home/HeadTitle.svelte";
import Message from "$lib/Loading/Message.svelte";
import Skeleton from "$lib/Loading/Skeleton.svelte";
import root from "$lib/Utility/root";
+import locale from "$stores/locale";
import "$styles/girls.scss";
</script>
-<HeadTitle route="Anime Girls Holding Programming Books" path="/girls" />
+<HeadTitle routeKey="girls" path="/girls" />
<div class="card">
<div class="split">
<div>
{#await Senpy.getRandomImage()}
- <Message message="Loading image ..." />
+ <Message message={$locale().routes?.girlsLoadingImage} />
<Skeleton grid={true} count={1} width="49%" height="16.25em" />
{:then randomImage}
<div class="preview">
<a href={randomImage.image} target="_blank">
- <img src={randomImage.image} alt="A random anime girl holding a programming book" />
+ <img src={randomImage.image} alt={$locale().routes?.girlsRandomAlt} />
</a>
</div>
{/await}
</div>
<div>
- The Senpy Club <span class="opaque">|</span> Anime Girls Holding Programming Books
+ {$locale().routes?.girlsIntroLeft} <span class="opaque">|</span> {$locale().routes?.girlsIntroRight}
<Spacer />
@@ -69,10 +70,10 @@ import "$styles/girls.scss";
<Spacer />
<details class="languages" open>
- <summary>Languages</summary>
+ <summary>{$locale().routes?.girlsLanguages}</summary>
{#await Senpy.getLanguages()}
- <Message message="Loading languages ..." />
+ <Message message={$locale().routes?.girlsLoadingLanguages} />
<Skeleton
card={false}
diff --git a/src/routes/girls/[language]/+page.svelte b/src/routes/girls/[language]/+page.svelte
index 2a97605f..4bef7f29 100644
--- a/src/routes/girls/[language]/+page.svelte
+++ b/src/routes/girls/[language]/+page.svelte
@@ -2,6 +2,7 @@
import Senpy from "$lib/Data/senpy";
import Message from "$lib/Loading/Message.svelte";
import Skeleton from "$lib/Loading/Skeleton.svelte";
+import locale from "$stores/locale";
import "$styles/girls.scss";
import type { PageData } from "./$types";
@@ -10,7 +11,7 @@ export let data: PageData;
<div class="card">
{#await Senpy.getImages(data.language)}
- <Message message="Loading images ..." />
+ <Message message={$locale().routes?.girlsLoadingImages} />
<Skeleton grid={true} count={1} width="49%" height="16.25em" />
{:then images}
@@ -18,7 +19,7 @@ export let data: PageData;
{#each images as image}
<a href={image} target="_blank">
<div class="preview">
- <img src={image} alt="An anime girl holding a programming book" />
+ <img src={image} alt={$locale().routes?.girlsSingleAlt} />
</div>
</a>
{/each}
diff --git a/src/routes/hololive/[[stream]]/+page.svelte b/src/routes/hololive/[[stream]]/+page.svelte
index 573c16aa..250ce22a 100644
--- a/src/routes/hololive/[[stream]]/+page.svelte
+++ b/src/routes/hololive/[[stream]]/+page.svelte
@@ -49,16 +49,16 @@ const getPinnedStreams = () => {
};
</script>
-<HeadTitle route="hololive Schedule" path="/hololive" />
+<HeadTitle routeKey="hololiveSchedule" path="/hololive" />
{#await schedulePromise}
- <Message message="Loading schedule ..." />
+ <Message message={$locale().hololive.loadingSchedule} />
<Skeleton grid={true} count={100} width="49%" height="16.25em" />
{:then scheduleResponse}
{#if scheduleResponse}
{#await scheduleResponse.text()}
- <Message message="Parsing schedule ..." />
+ <Message message={$locale().hololive.parsingSchedule} />
<Skeleton grid={true} count={100} width="49%" height="16.25em" />
{:then untypedSchedule}
@@ -68,17 +68,17 @@ const getPinnedStreams = () => {
{:catch}
<Message loader="ripple" slot>
{$locale().hololive.parseError}
- <a href={'#'} onclick={() => location.reload()}>Try again?</a>
+ <a href={'#'} onclick={() => location.reload()}>{$locale().hololive.tryAgainQuestion}</a>
</Message>
{/await}
{:else}
- <Message message="Loading schedule ..." />
+ <Message message={$locale().hololive.loadingSchedule} />
<Skeleton grid={true} count={100} width="49%" height="16.25em" />
{/if}
{:catch}
<Message loader="ripple" slot>
- {$locale().hololive.loadError} Please
- <a href={'#'} onclick={() => location.reload()}>try again</a> later.
+ {$locale().hololive.loadError} {$locale().hololive.pleasePrefix}
+ <a href={'#'} onclick={() => location.reload()}>{$locale().common?.tryAgain}</a> {$locale().hololive.laterSuffix}
</Message>
{/await}
diff --git a/src/routes/reader/+page.svelte b/src/routes/reader/+page.svelte
index b6cce066..f279a58e 100644
--- a/src/routes/reader/+page.svelte
+++ b/src/routes/reader/+page.svelte
@@ -10,16 +10,22 @@ import {
Resource,
} from "$lib/Reader/resource";
import InputTemplate from "$lib/Tools/InputTemplate.svelte";
+import locale from "$stores/locale";
let submission = "";
$: resourceIdentity = identify(submission);
</script>
-<InputTemplate field="Manga URL" bind:submission submitText="Read" preserveCase>
+<InputTemplate
+ field={$locale().reader?.mangaUrl ?? 'Manga URL'}
+ bind:submission
+ submitText={$locale().reader?.read ?? 'Read'}
+ preserveCase
+>
{#if resourceIdentity}
{#await fetchResource(submission)}
- <Message message="Loading chapters ..." />
+ <Message message={$locale().reader?.loadingChapters} />
{:then response}
{#if response.ok}
{#await decodeResource(response, submission) then data}
@@ -32,12 +38,12 @@ $: resourceIdentity = identify(submission);
<Notice>{error}</Notice>
{/await}
{:else}
- <Notice>Failed to fetch data</Notice>
+ <Notice>{$locale().reader?.fetchFailed}</Notice>
{/if}
{:catch}
- <Notice>An unknown error has occurred.</Notice>
+ <Notice>{$locale().reader?.unknownError}</Notice>
{/await}
{:else}
- <Notice>Invalid URL</Notice>
+ <Notice>{$locale().reader?.invalidUrl}</Notice>
{/if}
</InputTemplate>
diff --git a/src/routes/schedule/+page.svelte b/src/routes/schedule/+page.svelte
index 139f333e..9dcda20a 100644
--- a/src/routes/schedule/+page.svelte
+++ b/src/routes/schedule/+page.svelte
@@ -13,6 +13,7 @@ import Days from "$lib/Schedule/Days.svelte";
import Skeleton from "$lib/Loading/Skeleton.svelte";
import Message from "$lib/Loading/Message.svelte";
import subsPlease from "$stores/subsPlease";
+import locale from "$stores/locale";
import type { PageData } from "./$types";
export let data: PageData;
@@ -33,7 +34,7 @@ onMount(async () => {
});
</script>
-<HeadTitle route="Schedule" path="/schedule" />
+<HeadTitle routeKey="schedule" path="/schedule" />
<!-- <blockquote>
<select
@@ -62,12 +63,12 @@ onMount(async () => {
<Spacer /> -->
{#if !$subsPlease}
- <Message message="Loading subtitle schedule ..." />
+ <Message message={$locale().schedule?.loadingSubtitle} />
<Skeleton grid={true} count={7} height="15em" width="49.5%" />
{:else}
{#await scheduledMediaPromise}
- <Message message="Loading schedule ..." />
+ <Message message={$locale().schedule?.loadingSchedule} />
<Skeleton grid={true} count={7} height="15em" width="49.5%" />
{:then scheduledMedia}
@@ -76,7 +77,7 @@ onMount(async () => {
<Days subsPlease={$subsPlease} {scheduledMedia} {forceListMode} user={data.user} />
</div>
{:else}
- <Message message="Loading schedule ..." />
+ <Message message={$locale().schedule?.loadingSchedule} />
<Skeleton grid={true} count={7} height="15em" width="49.5%" />
{/if}
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte
index 054a126b..f9a2ee87 100644
--- a/src/routes/settings/+page.svelte
+++ b/src/routes/settings/+page.svelte
@@ -10,7 +10,6 @@ import Cache from "$lib/Settings/Categories/Cache.svelte";
import Category from "$lib/Settings/Category.svelte";
import tooltip from "$lib/Tooltip/tooltip";
import locale from "$stores/locale.js";
-import settings from "$stores/settings";
import LogInRestricted from "$lib/Error/LogInRestricted.svelte";
import SettingSync from "$lib/Settings/Categories/SettingSync.svelte";
import RssFeeds from "$lib/Settings/Categories/RSSFeeds.svelte";
@@ -28,28 +27,17 @@ export let data: PageData;
// };
</script>
-<HeadTitle route="Settings" path="/settings" />
+<HeadTitle routeKey="settings" path="/settings" />
<blockquote>
- {#if $settings.displayLanguage == 'en'}
- Have feedback or suggestions? Send a private message to
- <a
- href="https://anilist.co/user/fuwn"
- target="_blank"
- title={$locale().settings.tooltips.author}
- use:tooltip>@fuwn</a
- >
- on AniList!
- {:else if $settings.displayLanguage == 'ja'}
- フィードバックや提案はありますか?AniListで
- <a
- href="https://anilist.co/user/fuwn"
- target="_blank"
- title={$locale().settings.tooltips.author}
- use:tooltip>@fuwn</a
- >
- にDMを送ってください!
- {/if}
+ {$locale().routes?.settingsFeedbackPrefix}
+ <a
+ href="https://anilist.co/user/fuwn"
+ target="_blank"
+ title={$locale().settings.tooltips.author}
+ use:tooltip>@fuwn</a
+ >
+ {$locale().routes?.settingsFeedbackSuffix}
<!-- <Spacer />
diff --git a/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte
index 572f4fc5..3589fa9f 100644
--- a/src/routes/tools/+page.svelte
+++ b/src/routes/tools/+page.svelte
@@ -4,6 +4,7 @@ import HeadTitle from "$lib/Home/HeadTitle.svelte";
import Picker from "$lib/Tools/Picker.svelte";
import { tools } from "$lib/Tools/tools.js";
import root from "$lib/Utility/root";
+import locale from "$stores/locale";
let tool = "default";
</script>
@@ -36,9 +37,9 @@ let tool = "default";
<Spacer />
<blockquote style="margin: 0 0 0 1.5rem;">
- Have any requests for cool tools that you think others might find useful? Send a private message
- to
- <a href="https://anilist.co/user/fuwn" target="_blank" rel="noopener">@fuwn</a> on AniList!
+ {$locale().routes?.toolsFeedbackPrefix}
+ <a href="https://anilist.co/user/fuwn" target="_blank" rel="noopener">@fuwn</a>
+ {$locale().routes?.toolsFeedbackSuffix}
</blockquote>
</div>
diff --git a/src/routes/tools/[tool]/+page.svelte b/src/routes/tools/[tool]/+page.svelte
index ff764add..b74952a5 100644
--- a/src/routes/tools/[tool]/+page.svelte
+++ b/src/routes/tools/[tool]/+page.svelte
@@ -20,6 +20,7 @@ import Popup from "$lib/Layout/Popup.svelte";
import SequelCatcher from "$lib/Tools/SequelCatcher/Tool.svelte";
import Tracker from "$lib/Tools/Tracker/Tool.svelte";
import BirthdaysTemplate from "$lib/Tools/BirthdaysTemplate.svelte";
+import locale from "$stores/locale";
import type { PageData } from "./$types";
export let data: PageData;
@@ -38,15 +39,15 @@ $: if (tool === "girls") goto(root("/girls"));
<Picker bind:tool />
{#if !Object.keys(tools).includes(tool)}
- <HeadTitle route="Tools" path="/tools" />
+ <HeadTitle routeKey="tools" path="/tools" />
<Popup>
<p style="text-align: center;">
- Tool "<a href={root(`/tools/${tool}`)}>{tool}</a>" not found
+ {$locale().errors?.toolNotFoundPrefix}<a href={root(`/tools/${tool}`)}>{tool}</a>{$locale().errors?.toolNotFoundSuffix}
</p>
<blockquote style="margin: 0 0 0 1.5rem;">
- Did you mean "<a
+ {$locale().errors?.didYouMean} "<a
href={root(`/tools/${tools[suggestion].id}`)}
onclick={() => (tool = suggestion)}
style={suggestion === '...' ? 'pointer-events: none; color: inherit;' : ''}
diff --git a/src/routes/updates/+page.svelte b/src/routes/updates/+page.svelte
index 7dc628ca..71a6d2e3 100644
--- a/src/routes/updates/+page.svelte
+++ b/src/routes/updates/+page.svelte
@@ -5,6 +5,7 @@ import Skeleton from "$lib/Loading/Skeleton.svelte";
import { createHeightObserver } from "$lib/Utility/html";
import root from "$lib/Utility/root";
import { onDestroy, onMount } from "svelte";
+import locale from "$stores/locale";
let feed:
| { items: { title: string; link: string; content: string }[] }
@@ -64,17 +65,17 @@ const chapterTitle = (title: string) =>
title.replace(/^(.*?) (Vol\.|Ch\.|\bOneshot\b)/, "$2");
</script>
-<HeadTitle route="Updates" path="/updates" />
+<HeadTitle routeKey="updates" path="/updates" />
<div class="list-container">
<details open class="list">
<summary>
- Manga
+ {$locale().routes?.updatesManga}
<small class="opaque">{mangaEndTime ? mangaEndTime / 1000 : '...'}s</small>
</summary>
{#if feed === null}
- Failed to load feed
+ {$locale().routes?.updatesFailedToLoad}
{:else if feed !== undefined}
<ul>
{#each feed.items as item}
@@ -103,12 +104,12 @@ const chapterTitle = (title: string) =>
<details open class="list">
<summary>
- Novels
+ {$locale().routes?.updatesNovels}
<small class="opaque">{novelEndTime ? novelEndTime / 1000 : '...'}s</small>
</summary>
{#if novelFeed === null}
- Failed to load feed
+ {$locale().routes?.updatesFailedToLoad}
{:else if novelFeed !== undefined}
<ul>
{#each novelFeed.data.items as item}
diff --git a/src/routes/user/+page.svelte b/src/routes/user/+page.svelte
index 20a8d390..3fdf1dc6 100644
--- a/src/routes/user/+page.svelte
+++ b/src/routes/user/+page.svelte
@@ -29,4 +29,4 @@ onMount(async () => {
});
</script>
-<HeadTitle route="Profile" path="/user" />
+<HeadTitle routeKey="profile" path="/user" />
diff --git a/src/routes/user/[user]/+page.svelte b/src/routes/user/[user]/+page.svelte
index 1bcedc52..bfcdd87b 100644
--- a/src/routes/user/[user]/+page.svelte
+++ b/src/routes/user/[user]/+page.svelte
@@ -237,27 +237,29 @@ const toggleCategory = () => {
// 8.5827814569536423841e0
</script>
-<HeadTitle route={`${data.username}'s Profile`} path={`/user/${data.username}`} />
+<HeadTitle
+ route={$locale({ values: { username: data.username } }).headTitle?.userProfile}
+ path={`/user/${data.username}`}
+/>
{#if error}
<AnimeRateLimited>
- <a href={`https://anilist.co/user/${data.username}`} target="_blank">@{data.username}</a>'s
- profile could not be loaded.
+ <a href={`https://anilist.co/user/${data.username}`} target="_blank">@{data.username}</a>{$locale().errors?.profileCouldNotBeLoaded?.split('@{username}')[1]}
</AnimeRateLimited>
{:else}
{#if userData === null}
<Message slot withReload>
<p>
- Could not load user profile for <a
+ {$locale().user.profile.notLoaded?.split('@{username}')[0]}<a
href={`https://anilist.co/user/${data.username}`}
target="_blank">@{data.username}</a
- >.
+ >{$locale().user.profile.notLoaded?.split('@{username}')[1]}
</p>
</Message>
{:else if userData === undefined}
<Skeleton card={false} bigCard count={1} height="224px" />
- <Message message="Loading user profile ..." />
+ <Message message={$locale().user.profile.loadingProfile} />
{:else}
<div class="card card-small">
<div
@@ -297,10 +299,10 @@ const toggleCategory = () => {
</a>
{#if userData && authorisedUsers.includes(userData.id)}
&#8204;
- <button class="unclickable-button button-badge badge-rainbow">Owner</button>
+ <button class="unclickable-button button-badge badge-rainbow">{$locale().user.profile.owner}</button>
{/if}
<span class="click-item separator opaque">•</span>
- <a href={root(`/user/${userData.name}/badges`)}>Badge Wall</a>
+ <a href={root(`/user/${userData.name}/badges`)}>{$locale().user.profile.badgeWallLink}</a>
</p>
{#if preferences && preferences.biography && preferences.biography.length > 0}
@@ -362,7 +364,7 @@ const toggleCategory = () => {
>
<a href={root(`/hololive/${encodeURIComponent(stream)}`)}>
<div class="user-grid-hololive-badges">
- <ParallaxImage source={avatar} alternativeText="Avatar" />
+ <ParallaxImage source={avatar} alternativeText={$locale().navigation.avatar ?? 'Avatar'} />
</div>
</a>
</LinkedTooltip>
@@ -401,7 +403,7 @@ const toggleCategory = () => {
<Spacer />
- Pinned Categories
+ {$locale().user.profile.pinnedCategories}
<div class="pinned-categories">
{#each ownerPreferences.pinned_badge_wall_categories as category}
@@ -423,23 +425,28 @@ const toggleCategory = () => {
<button
onclick={() => {
if (userData) toggleCategoryQuery.mutate({ category }).then();
- }}>Remove</button
+ }}>{$locale().common?.remove}</button
>
</div>
{/each}
<span class="card card-small pinned-category">
<span class="pinned-category-name">
- <input type="text" id="category" placeholder="Category" style="width: 10em;" />
+ <input
+ type="text"
+ id="category"
+ placeholder={$locale().user.profile.categoryPlaceholder}
+ style="width: 10em;"
+ />
</span>
- <button class="button-lined" onclick={toggleCategory}>Add</button>
+ <button class="button-lined" onclick={toggleCategory}>{$locale().common?.add}</button>
</span>
</div>
<Spacer />
- Biography
+ {$locale().user.profile.biography}
<button
onclick={() => {
@@ -449,19 +456,19 @@ const toggleCategory = () => {
biography: getBiography()
})
.then();
- }}>Save</button
+ }}>{$locale().common?.save}</button
>
<textarea
value={ownerPreferences.biography}
rows="5"
cols="100"
id="biography"
- placeholder="Markdown supported!"
+ placeholder={$locale().user.profile.markdownPlaceholder}
></textarea>
<Spacer />
- Badge Wall Custom CSS
+ {$locale().user.profile.badgeWallCustomCss}
<button
onclick={() => {
@@ -471,14 +478,14 @@ const toggleCategory = () => {
css: getBadgeWallCSS()
})
.then();
- }}>Save</button
+ }}>{$locale().common?.save}</button
>
<textarea
value={ownerPreferences.badge_wall_css}
rows="10"
cols="100"
id="badgeWallCSS"
- placeholder="/* 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! */"
+ placeholder={$locale().user.profile.customCssPlaceholder}
></textarea>
</details>
{/if}
diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte
index 9ff81118..de55b456 100644
--- a/src/routes/user/[user]/badges/+page.svelte
+++ b/src/routes/user/[user]/badges/+page.svelte
@@ -12,6 +12,7 @@ import {
} from "$lib/Utility/time";
import proxy from "$lib/Utility/proxy";
import locale from "$stores/locale";
+import { get } from "svelte/store";
import Skeleton from "$lib/Loading/Skeleton.svelte";
import Message from "$lib/Loading/Message.svelte";
import Dropdown from "$lib/Layout/Dropdown.svelte";
@@ -209,7 +210,9 @@ type GroupedBadges = { [key: string]: IndexedBadge[] };
const setShadowHide = () => {
if (!badger) {
- loadError = "Something went wrong. Try refreshing.";
+ loadError =
+ get(locale)().badgeWall?.page?.somethingWentWrong ??
+ "Something went wrong. Try refreshing.";
return;
}
@@ -547,7 +550,9 @@ const shadowHideBadge = () => {
if (!selectedBadge && !authorised) return;
if (!badger) {
- loadError = "Something went wrong. Try refreshing.";
+ loadError =
+ get(locale)().badgeWall?.page?.somethingWentWrong ??
+ "Something went wrong. Try refreshing.";
return;
}
@@ -561,7 +566,10 @@ const shadowHideBadge = () => {
};
</script>
-<HeadTitle route={`${data.username}'s Badge Wall`} path={`/user/${data.username}`} />
+<HeadTitle
+ route={$locale({ values: { username: data.username } }).headTitle?.userBadgeWall}
+ path={`/user/${data.username}`}
+/>
{#if loadError}
<Popup fullscreen locked>
@@ -571,11 +579,11 @@ const shadowHideBadge = () => {
{@const isOwner = $identity && (isId ? $identity.id : $identity.name) === data.username}
{#if $BadgeWallUser.fetching || !$BadgeWallUser.data}
- <Message message="Loading badges ..." />
+ <Message message={$locale().badgeWall?.page?.loadingBadges} />
<Skeleton grid={true} count={100} width="150px" height="170px" />
{:else if !$BadgeWallUser.data.User}
- <Message message="No badges yet." />
+ <Message message={$locale().badgeWall?.page?.noBadgesYet} />
{:else}
{@const ungroupedBadges = castBadgesToIndexedBadges($BadgeWallUser.data.User.badges)}
{@const isBadgeSelected =
@@ -591,7 +599,7 @@ const shadowHideBadge = () => {
{/if}
{#if ungroupedBadges === null}
- <Message message="Loading badges ..." />
+ <Message message={$locale().badgeWall?.page?.loadingBadges} />
<Skeleton grid={true} count={10} width="150px" height="170px" />
{:else}
@@ -605,23 +613,17 @@ const shadowHideBadge = () => {
{#if shadowHidden}
<div class="card">
- <b>Notice:</b> The Badge Wall overseer system has detected badges containing
- AI-generated material on your wall. {shadowHiddenCount} of your badges have been shadow
- hidden.
+ <b>{$locale().badgeWall?.page?.notice}</b>
+ {$locale({ values: { count: shadowHiddenCount } }).badgeWall?.page?.shadowHideNotice1}
<Spacer />
- 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.
+ {$locale().badgeWall?.page?.shadowHideNotice2}
</div>
{:else if false && !noticeDismissed}
<div class="card">
- <b>Notice:</b> 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.
+ <b>{$locale().badgeWall?.page?.notice}</b>
+ {$locale().badgeWall?.page?.aiNotice1}
<Spacer />
- Failure to comply with this request at your earliest convenience will result in the hiding
- of all badges from your Badge Wall.
+ {$locale().badgeWall?.page?.aiNotice2}
<Spacer />
<button
onclick={async () => {
@@ -630,7 +632,7 @@ const shadowHideBadge = () => {
await localforage.setItem('badgeWallNoticeDismissed', 'true');
}}
>
- Dismiss
+ {$locale().badgeWall?.page?.dismiss}
</button>
</div>
{/if}
@@ -639,7 +641,7 @@ const shadowHideBadge = () => {
<div class="card">
{#if authorised}
- <button onclick={setShadowHide}>Shadow Hide Badges</button>
+ <button onclick={setShadowHide}>{$locale().badgeWall?.page?.shadowHide}</button>
{/if}
{#if isOwner && authorised}
@@ -675,7 +677,7 @@ const shadowHideBadge = () => {
migrateMode = !migrateMode;
}}
>
- Migrate Category
+ {$locale().badgeWall?.page?.migrateCategory}
</button>
<span style="margin: 0 0.625rem;">•</span>
<button
@@ -684,14 +686,14 @@ const shadowHideBadge = () => {
hideMode = !hideMode;
}}
>
- Hide Category
+ {$locale().badgeWall?.page?.hideCategory}
</button>
<!-- <!-- <span style="margin: 0 0.625rem;">•</span> -->
<!-- <button onclick={() => exportBadges(groupedBadges)}>Export Badges</button> -->
{#if shadowHidden}
<span style="margin: 0 0.625rem;">•</span>
- <button onclick={setShadowHide}>Un-shadow Hide Badges</button>
+ <button onclick={setShadowHide}>{$locale().badgeWall?.page?.unshadowHide}</button>
{/if}
{#if editMode && isOwner}
@@ -784,7 +786,7 @@ const shadowHideBadge = () => {
? dateToInputTime(databaseTimeToDate(selectedBadge.time))
: ''}
/>
- <small>Must be full date and time, defaults to now if any fields empty</small>
+ <small>{$locale().badgeWall?.page?.dateTimeHint}</small>
</span>
<Spacer />
@@ -827,13 +829,17 @@ const shadowHideBadge = () => {
</Dropdown>
<Dropdown
items={[false, true].map((hidden) => ({
- name: hidden ? 'Hidden' : 'Shown',
+ name: hidden
+ ? ($locale().badgeWall?.page?.hidden ?? 'Hidden')
+ : ($locale().badgeWall?.page?.shown ?? 'Shown'),
url: '#',
onClick: () => {
const hiddenInput = document.querySelector('input[name="hidden"]');
if (hiddenInput instanceof HTMLInputElement)
- hiddenInput.value = hidden ? 'Hidden' : 'Shown';
+ hiddenInput.value = hidden
+ ? ($locale().badgeWall?.page?.hidden ?? 'Hidden')
+ : ($locale().badgeWall?.page?.shown ?? 'Shown');
}
}))}
header={false}
@@ -842,16 +848,16 @@ const shadowHideBadge = () => {
<span slot="title">
<input
type="text"
- placeholder="Shown"
+ placeholder={$locale().badgeWall?.page?.shown}
name="hidden"
minlength="1"
maxlength="1000"
size="15"
value={selectedBadge
? selectedBadge.hidden
- ? 'Hidden'
- : 'Shown'
- : 'Shown'}
+ ? ($locale().badgeWall?.page?.hidden ?? 'Hidden')
+ : ($locale().badgeWall?.page?.shown ?? 'Shown')
+ : ($locale().badgeWall?.page?.shown ?? 'Shown')}
/>
</span>
</Dropdown>
@@ -879,9 +885,11 @@ const shadowHideBadge = () => {
{#if loadQueryParameter === 'none'}
<div class="card">
- <b>Notice:</b>
- {ungroupedBadges.length} badges have been loaded successfully, but they are not being displayed
- due to your preferences (<code>load=none</code>).
+ <b>{$locale().badgeWall?.page?.notice}</b>
+ {$locale({
+ values: { count: ungroupedBadges.length }
+ }).badgeWall?.page?.loadNoneNoticePrefix}<code>load=none</code>{$locale().badgeWall
+ ?.page?.loadNoneNoticeSuffix}
</div>
{:else}
<Badges
@@ -919,10 +927,12 @@ const shadowHideBadge = () => {
{#if authorised}
<button onclick={shadowHideBadge}>
{#if selectedBadge && selectedBadge.shadow_hidden}
- Un-shadow
+ {$locale({ values: { id: selectedBadge.id } }).badgeWall?.page?.unshadowHideBadge}
{:else}
- Shadow
- {/if} Hide Badge ({selectedBadge ? selectedBadge.id : 0})
+ {$locale({
+ values: { id: selectedBadge ? selectedBadge.id : 0 }
+ }).badgeWall?.page?.shadowHideBadge}
+ {/if}
</button>
{/if}
</Popup>
@@ -1026,13 +1036,13 @@ const shadowHideBadge = () => {
{/if}
<Popup fullscreen onLeave={() => (migrateMode = false)} show={migrateMode}>
- Migrate Category
+ {$locale().badgeWall?.page?.migrateCategory}
<Spacer />
<input
type="text"
- placeholder="Original Category"
+ placeholder={$locale().badgeWall?.page?.originalCategoryPlaceholder}
id="migrate_original"
minlength="1"
maxlength="1000"
@@ -1040,13 +1050,13 @@ const shadowHideBadge = () => {
/>
<input
type="text"
- placeholder="New Category"
+ placeholder={$locale().badgeWall?.page?.newCategoryPlaceholder}
id="migrate_new"
minlength="1"
maxlength="1000"
size="20"
/>
- <SettingHint lineBreak>Leave category empty to migrate all to or from uncategorised.</SettingHint>
+ <SettingHint lineBreak>{$locale().badgeWall?.page?.migrateAllHint}</SettingHint>
<Spacer />
@@ -1060,29 +1070,28 @@ const shadowHideBadge = () => {
{$locale().user.badges.importMode.cancel}
</button>
<button onclick={() => migrateCategory()} class="button-lined" style="float: right;">
- Migrate
+ {$locale().badgeWall?.page?.migrateAction}
</button>
</Popup>
<Popup fullscreen onLeave={() => (hideMode = false)} show={hideMode}>
- Hide Category
+ {$locale().badgeWall?.page?.hideCategory}
<SettingHint lineBreak>
- If the majority of the badges in a category are shown, the category will be hidden, and vice
- versa.
+ {$locale().badgeWall?.page?.hideVisibilityHint}
</SettingHint>
<Spacer />
<input
type="text"
- placeholder="Category"
+ placeholder={$locale().badgeWall?.page?.categoryPlaceholder}
id="category_hide"
minlength="1"
maxlength="1000"
size="20"
/>
- <SettingHint lineBreak>Leave category field empty to hide all.</SettingHint>
+ <SettingHint lineBreak>{$locale().badgeWall?.page?.hideAllHint}</SettingHint>
<Spacer />
@@ -1096,6 +1105,6 @@ const shadowHideBadge = () => {
{$locale().user.badges.importMode.cancel}
</button>
<button onclick={() => hideCategory()} class="button-lined" style="float: right;"
- >Toggle Visibility</button
+ >{$locale().badgeWall?.page?.toggleVisibility}</button
>
</Popup>