From 56a7a7851b09cb30a5cd543c8cb4f926109b4290 Mon Sep 17 00:00:00 2001
From: Fuwn <contact@fuwn.net>
Date: Sun, 24 May 2026 13:22:34 +0000
Subject: refactor(locale): move hardcoded UI strings into english locale

Adds optional namespaces (common, errors, commandPalette, headTitle,
notifications, schedule, events, home, reader, routes, badgePreview,
badgeWall) and extends existing ones (settings.*, lists.*, tools.*,
user.*, hololive.*) on the Locale interface. New fields are optional
so japanese.ts can omit them; svelte-i18n's fallbackLocale handles
the runtime miss.

HeadTitle gains an optional routeKey prop for type-safe lookup.
defaultActions becomes a factory so the command palette re-reads
locale on language toggle. The existing JP feedback translation
in routes/settings is preserved via japanese.ts.

Out of scope (kept hardcoded): service-worker.ts, app.html,
Landing*.svelte, tools.ts registry, Easter Event 2025 pages.
---
 src/lib/Tools/Wrapped/Tool.svelte | 141 +++++++++++++++++++++-----------------
 1 file changed, 80 insertions(+), 61 deletions(-)

(limited to 'src/lib/Tools/Wrapped/Tool.svelte')

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}
-- 
cgit v1.2.3

