From 2e7f198a8108c53c6162be0d156edfce85195aa3 Mon Sep 17 00:00:00 2001
From: Joseph Eiba <josepheiba@icloud.com>
Date: Tue, 30 Sep 2025 15:26:40 +0100
Subject: feat: Add initial language support for English and Japanese

- Add translation framework for UI elements
- Implement language switching functionality
- Add English and Japanese translations for UI components
- Card names and effects translation to be implemented in future commits
---
 src/components/left_panel.svelte  |   5 +-
 src/components/loading.svelte     |   3 +-
 src/components/main_panel.svelte  |  41 +++++++-----
 src/components/right_panel.svelte |   7 ++-
 src/language.js                   |  31 ++++++++++
 src/loading.js                    |   2 +
 src/translations.js               | 127 ++++++++++++++++++++++++++++++++++++++
 7 files changed, 193 insertions(+), 23 deletions(-)
 create mode 100644 src/language.js
 create mode 100644 src/translations.js

diff --git a/src/components/left_panel.svelte b/src/components/left_panel.svelte
index a2c14c5..bcfdba7 100644
--- a/src/components/left_panel.svelte
+++ b/src/components/left_panel.svelte
@@ -9,6 +9,7 @@
     } from '../left_panel';
 
     import { cardImageUrl } from '../utils';
+    import { currentTranslations } from '../language';
 
 
 </script>
@@ -24,8 +25,8 @@
             <pre class="card-desc-text">{$leftPanelCardDesc}</pre>
             <div style="height:3em;line-height:1.0;overflow:hidden;">
                 <div style="break-inside:avoid;"><p style="text-align:center;"><small>
-                    <a class="link" href="https://github.com/mistivia/ygo-deck-builder">源代码</a>
-                    <br>关注 <a class="link" href="https://mistivia.com">Mistivia</a> 谢谢喵~
+                    <a class="link" href="https://github.com/mistivia/ygo-deck-builder">{$currentTranslations.sourceCode}</a>
+                    <br>{$currentTranslations.followMistivia} <a class="link" href="https://mistivia.com">Mistivia</a> {$currentTranslations.thankYou}
                 </small></p></div>
             </div>
         </div>
diff --git a/src/components/loading.svelte b/src/components/loading.svelte
index fb49495..42b3404 100644
--- a/src/components/loading.svelte
+++ b/src/components/loading.svelte
@@ -1,5 +1,6 @@
 <script>
     import { isLoading } from '../loading';
+    import { currentTranslations } from '../language';
 </script>
 
 {#if $isLoading}
@@ -7,7 +8,7 @@
         <div class="loading-content">
             <div class="loading-spinner"></div>
             <br>
-            <p>加载中...</p>
+            <p>{$currentTranslations.loading}</p>
         </div>
     </div>
 {/if}
diff --git a/src/components/main_panel.svelte b/src/components/main_panel.svelte
index 0d959c2..9029a16 100644
--- a/src/components/main_panel.svelte
+++ b/src/components/main_panel.svelte
@@ -8,6 +8,7 @@
         genYdke,
         downloadStringAsFile,
     } from '../utils';
+    import { language, setLanguage, currentTranslations } from '../language';
 
     let fileInput;
     
@@ -44,10 +45,10 @@
         let deckString = genYdk($deck);
         navigator.clipboard.writeText(deckString)
             .then(() => {
-              alert('YDK卡组码已复制到剪贴板');
+              alert($currentTranslations.ydkCopied);
             })
             .catch(err => {
-              alert("失败！");
+              alert($currentTranslations.failed);
             });
     }
 
@@ -57,10 +58,10 @@
         url = url + '#' + genYdke($deck);
         navigator.clipboard.writeText(url)
             .then(() => {
-              alert('分享链接已复制到剪贴板');
+              alert($currentTranslations.shareLinkCopied);
             })
             .catch(err => {
-              alert("失败！");
+              alert($currentTranslations.failed);
             });
     }
 
@@ -91,27 +92,32 @@
 
 <div class="middle-panel">
     <div class="control-bar">
-        <button class="btn" onclick={openDeck}>打开</button>
-        <button class="btn" onclick={saveDeck}>保存</button>
-        <button class="btn" onclick={clearDeck}>清空</button>
-        <button class="btn" onclick={copyDeck}>复制到剪贴板</button>
-        <button class="btn" onclick={shareDeck}>分享</button>
+        <button class="btn" onclick={openDeck}>{$currentTranslations.open}</button>
+        <button class="btn" onclick={saveDeck}>{$currentTranslations.save}</button>
+        <button class="btn" onclick={clearDeck}>{$currentTranslations.clear}</button>
+        <button class="btn" onclick={copyDeck}>{$currentTranslations.copyToClipboard}</button>
+        <button class="btn" onclick={shareDeck}>{$currentTranslations.share}</button>
+        <select bind:value={$language} class="select-language" id="language" onchange={()=>setLanguage($language)}>
+            <option value="chinese">中文</option>
+            <option value="english">English</option>
+            <option value="japanese">日本語</option>
+        </select>
         <select bind:value={$format} class="select-format" id="format" onchange={()=>setFormat($format)}>
-            <option value="none">无禁限</option>
+            <option value="none">{$currentTranslations.noLimit}</option>
             <option value="ocg">OCG</option>
             <option value="tcg">TCG</option>
-            <option value="md">大师决斗</option>
-            <option value="cnocg">简中</option>
+            <option value="md">{$currentTranslations.masterDuel}</option>
+            <option value="cnocg">{$currentTranslations.cnSimplified}</option>
             <option value="genesys">Genesys</option>
         </select>
         {#if $format === 'genesys'}
-            <span>点数：{$deck.point} </span>
+            <span>{$currentTranslations.points}{$deck.point} </span>
         {/if}
 
     </div>
     <div class="deck-section">
         <div class="deck-group">
-            <h3>主卡组（{$deck.main.length}）</h3>
+            <h3>{$currentTranslations.mainDeck}（{$deck.main.length}）</h3>
             <div role="region" ondragover={(e)=>e.preventDefault()} ondrop={(e)=>onDrop("main", e, -1)} class="card-grid main-deck">
                 {#each $deck.main as card, i}
                     <div class="card-grid-thumb" role="region" ondragover={(e)=>e.preventDefault()} ondrop={(e)=>onDrop("main", e, i)}>
@@ -121,7 +127,7 @@
             </div>
         </div>
         <div class="deck-group">
-            <h3>额外卡组（{$deck.extra.length}）</h3>
+            <h3>{$currentTranslations.extraDeck}（{$deck.extra.length}）</h3>
             <div role="region" ondragover={(e)=>e.preventDefault()} ondrop={(e)=>onDrop("extra", e, -1)} class="card-grid extra-deck">
                 {#each $deck.extra as card, i}
                     <div class="card-grid-thumb" role="region" ondragover={(e)=>e.preventDefault()} ondrop={(e)=>onDrop("extra", e, i)}>
@@ -131,7 +137,7 @@
             </div>
         </div>
         <div class="deck-group">
-            <h3>副卡组（{$deck.side.length}）</h3>
+            <h3>{$currentTranslations.sideDeck}（{$deck.side.length}）</h3>
             <div role="region" ondragover={(e)=>e.preventDefault()} ondrop={(e)=>onDrop("side", e, -1)} class="card-grid side-deck">
                 {#each $deck.side as card, i}
                     <div class="card-grid-thumb" role="region" ondragover={(e)=>e.preventDefault()} ondrop={(e)=>onDrop("side", e, i)}>
@@ -172,7 +178,8 @@
     }
     
     
-    .select-format {
+    .select-format,
+    .select-language {
         padding: 8px 8px;
         margin-right: 10px;
         cursor: pointer;
diff --git a/src/components/right_panel.svelte b/src/components/right_panel.svelte
index b0b8553..28f4307 100644
--- a/src/components/right_panel.svelte
+++ b/src/components/right_panel.svelte
@@ -3,6 +3,7 @@
     import { changeInput, showingCards, onPrevPage, onNextPage } from '../search'
     import { deckOps, format } from '../deck';
     import { cardLimit, cornerMark } from '../card_db';
+    import { currentTranslations } from '../language';
 
     function onChange(event) {
         changeInput(event.target.value);
@@ -19,7 +20,7 @@
 
 <div class="right-panel" role="region" ondragover={(e)=>e.preventDefault()} ondrop={onDrop}>
     <div class="search-bar">
-        <input type="text" placeholder="搜索卡牌..." oninput={onChange}>
+        <input type="text" placeholder={$currentTranslations.searchPlaceholder} oninput={onChange}>
     </div>
     <div class="card-list">
            {#each $showingCards as card}
@@ -32,8 +33,8 @@
            {/each}
     </div>
     <div class="pagination">
-        <button class="page-btn" onclick={onPrevPage}>上一页</button>
-        <button class="page-btn" onclick={onNextPage}>下一页</button>
+        <button class="page-btn" onclick={onPrevPage}>{$currentTranslations.prevPage}</button>
+        <button class="page-btn" onclick={onNextPage}>{$currentTranslations.nextPage}</button>
     </div>
 </div>
 
diff --git a/src/language.js b/src/language.js
new file mode 100644
index 0000000..b4b9605
--- /dev/null
+++ b/src/language.js
@@ -0,0 +1,31 @@
+import { writable, derived } from "svelte/store";
+import translations from './translations.js';
+
+let defaultLanguage = 'chinese';
+let language = writable(defaultLanguage);
+let languageState = defaultLanguage;
+
+function setLanguage(newLanguage) {
+    localStorage.setItem('language', newLanguage);
+    languageState = newLanguage;
+    language.set(newLanguage);
+}
+
+function initLanguage() {
+    let cachedLanguage = localStorage.getItem('language');
+    if (cachedLanguage !== null) {
+        setLanguage(cachedLanguage);
+    }
+}
+
+const currentTranslations = derived(language, ($language) => {
+    return translations[$language] || translations.chinese;
+});
+
+export {
+    language,
+    languageState,
+    setLanguage,
+    initLanguage,
+    currentTranslations,
+};
\ No newline at end of file
diff --git a/src/loading.js b/src/loading.js
index 523f26c..94fb08c 100644
--- a/src/loading.js
+++ b/src/loading.js
@@ -1,6 +1,7 @@
 import { writable } from 'svelte/store';
 import { initSearch } from './search';
 import { initDeck } from './deck';
+import { initLanguage } from './language';
 import { setCardDb, setAltId } from './card_db';
 import idChangelog from './id_changelog.json';
 
@@ -53,6 +54,7 @@ async function setidxdbitem(key, value) {
 }
 
 async function fetchCardDb() {
+    initLanguage(); // Load language before showing loading screen
     let localVer = localStorage.getItem('card_db_ver');
     try {
         // load card db
diff --git a/src/translations.js b/src/translations.js
new file mode 100644
index 0000000..af430cb
--- /dev/null
+++ b/src/translations.js
@@ -0,0 +1,127 @@
+const translations = {
+    chinese: {
+        // Buttons
+        open: "打开",
+        save: "保存",
+        clear: "清空",
+        copyToClipboard: "复制到剪贴板",
+        share: "分享",
+        
+        // Format options
+        noLimit: "无禁限",
+        masterDuel: "大师决斗",
+        cnSimplified: "简中",
+        
+        // Deck sections
+        mainDeck: "主卡组",
+        extraDeck: "额外卡组",
+        sideDeck: "副卡组",
+        
+        // Search
+        searchPlaceholder: "搜索卡牌...",
+        
+        // Pagination
+        prevPage: "上一页",
+        nextPage: "下一页",
+        
+        // Points display
+        points: "点数：",
+        
+        // Footer links
+        sourceCode: "源代码",
+        followMistivia: "关注",
+        thankYou: "谢谢喵~",
+        
+        // Alert messages
+        ydkCopied: "YDK卡组码已复制到剪贴板",
+        shareLinkCopied: "分享链接已复制到剪贴板",
+        failed: "失败！",
+        
+        // Loading
+        loading: "加载中..."
+    },
+    english: {
+        // Buttons
+        open: "Open",
+        save: "Save",
+        clear: "Clear",
+        copyToClipboard: "Copy to Clipboard",
+        share: "Share",
+        
+        // Format options
+        noLimit: "No Limit",
+        masterDuel: "Master Duel",
+        cnSimplified: "Chinese OCG",
+        
+        // Deck sections
+        mainDeck: "Main Deck",
+        extraDeck: "Extra Deck",
+        sideDeck: "Side Deck",
+        
+        // Search
+        searchPlaceholder: "Search cards...",
+        
+        // Pagination
+        prevPage: "Previous",
+        nextPage: "Next",
+        
+        // Points display
+        points: "Points: ",
+        
+        // Footer links
+        sourceCode: "Source Code",
+        followMistivia: "Follow",
+        thankYou: "Thank you~",
+        
+        // Alert messages
+        ydkCopied: "YDK deck code copied to clipboard",
+        shareLinkCopied: "Share link copied to clipboard",
+        failed: "Failed!",
+        
+        // Loading
+        loading: "Loading..."
+    },
+    japanese: {
+        // Buttons
+        open: "開く",
+        save: "保存",
+        clear: "クリア",
+        copyToClipboard: "クリップボードにコピー",
+        share: "シェア",
+        
+        // Format options
+        noLimit: "制限なし",
+        masterDuel: "マスターデュエル",
+        cnSimplified: "簡体字中国語",
+        
+        // Deck sections
+        mainDeck: "メインデッキ",
+        extraDeck: "エクストラデッキ",
+        sideDeck: "サイドデッキ",
+        
+        // Search
+        searchPlaceholder: "カードを検索...",
+        
+        // Pagination
+        prevPage: "前のページ",
+        nextPage: "次のページ",
+        
+        // Points display
+        points: "ポイント：",
+        
+        // Footer links
+        sourceCode: "ソースコード",
+        followMistivia: "フォロー",
+        thankYou: "ありがとうにゃ〜",
+        
+        // Alert messages
+        ydkCopied: "YDKデッキコードをクリップボードにコピーしました",
+        shareLinkCopied: "シェアリンクをクリップボードにコピーしました",
+        failed: "失敗！",
+        
+        // Loading
+        loading: "読み込み中..."
+    }
+};
+
+export default translations;
\ No newline at end of file
-- 
cgit v1.0

