Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@

.vscode/settings.json
*.ffs_db
*.ffs_gui
*.log
node_modules/
18 changes: 18 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<link rel="stylesheet" href="style.css" />
<link href="https://tauhu.tw/tauhu-oo.css" rel="stylesheet" />

<script type="text/javascript" src="js/translation/data_processor.js" defer></script>
<script type="text/javascript" src="js/translation/phonology.js" defer></script>
<script type="text/javascript" src="js/translation/engine.js" defer></script>
<script type="text/javascript" src="main.js" defer></script>
<script type="text/javascript" src="js/romanizer.js" defer></script>
<script
Expand Down Expand Up @@ -378,10 +381,25 @@ <h4 id="selectionPopupTitle">擇詞讀音結果</h4>
<button id="selectionPopupRomanizerBtn" class="popup-romanizer-btn" title="用 Romanizer 處理">
<i class="fas fa-pen-to-square"></i> Romanizer
</button>
<button id="selectionPopupTranslateBtn" class="popup-translate-btn" title="翻譯這段文本">
<i class="fas fa-language"></i> 翻譯
</button>
</div>
<div id="selectionPopupContent">
<!-- JavaScript will fill this -->
</div>
<div id="translationPopupContent" style="display: none;">
<button id="backToPronunciationBtn" title="返回發音"><i class="fas fa-arrow-left"></i></button>
<p id="translationResult"></p>
<select id="targetDialectSelector">
<option value="海陸">海陸</option>
<option value="四縣">四縣</option>
<option value="大埔">大埔</option>
<option value="饒平">饒平</option>
<option value="詔安">詔安</option>
<option value="南四縣">南四縣</option>
</select>
</div>
</div>

<!-- Info Modal -->
Expand Down
29 changes: 29 additions & 0 deletions js/translation/data_processor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// js/translation/data_processor.js

/**
* Fetches and processes the Hakka dictionary and vocabulary data.
*/
class DataProcessor {
constructor() {
this.data = {};
}

/**
* Loads all the necessary data files.
*/
async loadAllData() {
// In the future, we will load data from the data/ directory.
// For now, we can use placeholder data.
console.log("DataProcessor: Loading data...");
// This is where the data loading logic will go.
console.log("DataProcessor: Data loaded.");
}

/**
* Gets the processed data.
* @returns {object} The processed data.
*/
getData() {
return this.data;
}
}
38 changes: 38 additions & 0 deletions js/translation/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// js/translation/engine.js

/**
* Orchestrates the translation process.
*/
class TranslationEngine {
constructor() {
this.dataProcessor = new DataProcessor();
this.phonology = new Phonology();
}

/**
* Initializes the translation engine.
*/
async initialize() {
await this.dataProcessor.loadAllData();
}

/**
* Translates text from a source dialect to a target dialect.
* @param {string} text - The text to translate.
* @param {string} sourceDialect - The source dialect.
* @param {string} targetDialect - The target dialect.
* @returns {string} The translated text.
*/
translate(text, sourceDialect, targetDialect) {
console.log(`TranslationEngine: Translating "${text}" from ${sourceDialect} to ${targetDialect}...`);

// This is a placeholder implementation.
// The actual implementation will involve looking up the text,
// getting its phonological representation, translating the phonology,
// and then finding the corresponding word in the target dialect.

const translatedText = `Translated: ${text}`; // Placeholder
console.log(`TranslationEngine: Translated text is "${translatedText}".`);
return translatedText;
}
}
25 changes: 25 additions & 0 deletions js/translation/phonology.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// js/translation/phonology.js

/**
* Handles the phonological derivation between Hakka dialects.
*/
class Phonology {
constructor() {
// This is where we will store the phonological rules.
this.rules = {};
}

/**
* Translates a phonological representation from a source dialect to a target dialect.
* @param {string} phonology - The phonological representation to translate.
* @param {string} sourceDialect - The source dialect.
* @param {string} targetDialect - The target dialect.
* @returns {string} The translated phonological representation.
*/
translate(phonology, sourceDialect, targetDialect) {
console.log(`Phonology: Translating ${phonology} from ${sourceDialect} to ${targetDialect}...`);
// This is where the translation logic will go.
// For now, we can just return the original phonology.
return phonology;
}
}
32 changes: 32 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ let g_currentLevelData = []; // Store the full data for the current level
let audioAbortController = new AbortController();
let playbackSessionId = null; // <-- 【新增此行】
let g_currentSearchResults = [];
let translationEngine = null;

const LEVEL_TO_EXCEPTION_FILE = {
'基': '基例外音檔',
Expand Down Expand Up @@ -979,6 +980,34 @@ function showPronunciationPopup(selectedText, readings, anchorElementOrRect, cal
}
};
}

const translateBtn = document.getElementById('selectionPopupTranslateBtn');
if (translateBtn) {
translateBtn.onclick = async (e) => {
e.stopPropagation();
if (translationEngine) {
const targetDialect = document.getElementById('targetDialectSelector').value;
const translatedText = translationEngine.translate(selectedText, contextualDialect, targetDialect);
const selectionPopupContent = document.getElementById('selectionPopupContent');
const translationPopupContent = document.getElementById('translationPopupContent');
const translationResult = document.getElementById('translationResult');
selectionPopupContent.style.display = 'none';
translationPopupContent.style.display = 'block';
translationResult.textContent = translatedText;
}
};
}

const backToPronunciationBtn = document.getElementById('backToPronunciationBtn');
if (backToPronunciationBtn) {
backToPronunciationBtn.onclick = (e) => {
e.stopPropagation();
const selectionPopupContent = document.getElementById('selectionPopupContent');
const translationPopupContent = document.getElementById('translationPopupContent');
selectionPopupContent.style.display = 'block';
translationPopupContent.style.display = 'none';
};
}
}

function hidePronunciationPopup(popupEl, backdropEl) {
Expand Down Expand Up @@ -1636,6 +1665,9 @@ async function initializeApp() {
}

await loadDataFromDB(db);

translationEngine = new TranslationEngine();
await translationEngine.initialize();

initializeAppUI();

Expand Down
56 changes: 56 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"playwright": "^1.57.0"
}
}
4 changes: 4 additions & 0 deletions test-results/.last-run.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"status": "failed",
"failedTests": []
}
55 changes: 55 additions & 0 deletions tests/verify_translation_flow.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

const { test, expect } = require('@playwright/test');

test.describe('Translation UI Flow Verification', () => {
test('should correctly navigate the translation popup UI', async ({ page }) => {
// 1. Navigate to the application
await page.goto('http://localhost:8000');

// 2. Wait for the main content to be visible to ensure the app is loaded
await page.waitForSelector('#main-content', { state: 'visible', timeout: 30000 });

// 3. Find the first sentence element and select text within it
const sentenceElement = await page.locator('.sentence').first();
await expect(sentenceElement).toBeVisible();

// Simulate a mouse drag to select text, as click/select_text doesn't trigger the mouseup event
const boundingBox = await sentenceElement.boundingBox();
if (!boundingBox) {
throw new Error('Could not get bounding box for sentence element');
}
await page.mouse.move(boundingBox.x + boundingBox.width / 4, boundingBox.y + boundingBox.height / 2);
await page.mouse.down();
await page.mouse.move(boundingBox.x + (boundingBox.width * 3) / 4, boundingBox.y + boundingBox.height / 2);
await page.mouse.up();

// 4. Wait for the selection popup to appear and verify the "Translate" button is present
const popup = page.locator('#selection-popup');
await expect(popup).toBeVisible({ timeout: 10000 });
const translateButton = popup.locator('#translate-btn');
await expect(translateButton).toBeVisible();

// 5. Click the "Translate" button
await translateButton.click();

// 6. Verify the translation view is now active
const translationView = popup.locator('#translation-view');
await expect(translationView).toBeVisible();
const backButton = popup.locator('#back-to-pronunciation-btn');
await expect(backButton).toBeVisible();

// 7. Take a screenshot of the translation view
await page.screenshot({ path: '/home/jules/verification/translation_view.png' });

// 8. Click the "Back" button
await backButton.click();

// 9. Verify the original pronunciation view is restored
const pronunciationView = popup.locator('#pronunciation-view');
await expect(pronunciationView).toBeVisible();
await expect(translationView).toBeHidden();

// 10. Take a final screenshot to confirm the state is restored
await page.screenshot({ path: '/home/jules/verification/final_view.png' });
});
});