fix(reader): fix reader word popup translation key and sanitize token lookup
This commit is contained in:
@@ -273,6 +273,9 @@ async def lookup_term(query: str) -> Dict[str, Any]:
|
|||||||
result["translation"] = mm_data.get("translation", "")
|
result["translation"] = mm_data.get("translation", "")
|
||||||
result["translations_list"] = [result["translation"]] if result["translation"] else []
|
result["translations_list"] = [result["translation"]] if result["translation"] else []
|
||||||
|
|
||||||
|
# Alias translation_ru for client compatibility
|
||||||
|
result["translation_ru"] = result["translation"]
|
||||||
|
|
||||||
# Store into SQLite cache
|
# Store into SQLite cache
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>Notes — Персональный тренажер и словарь английского языка</title>
|
<title>Notes — Персональный тренажер и словарь английского языка</title>
|
||||||
<meta name="description" content="Защищённый персональный тренажёр и словарь английского языка с интервальными повторениями SRS SM-2, контекстным поиском и озвучкой">
|
<meta name="description" content="Защищённый персональный тренажёр и словарь английского языка с интервальными повторениями SRS SM-2, контекстным поиском и озвучкой">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css?v=2.2">
|
||||||
|
|
||||||
<!-- PWA meta and icons -->
|
<!-- PWA meta and icons -->
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
@@ -922,6 +922,6 @@
|
|||||||
<!-- Audio Element for native streaming -->
|
<!-- Audio Element for native streaming -->
|
||||||
<audio id="global-audio-player" preload="none"></audio>
|
<audio id="global-audio-player" preload="none"></audio>
|
||||||
|
|
||||||
<script src="/static/js/app.js"></script>
|
<script src="/static/js/app.js?v=2.2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+11
-6
@@ -1897,7 +1897,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Tokenize words while preserving spaces & punctuation
|
// Tokenize words while preserving spaces & punctuation
|
||||||
const tokens = paraText.split(/([A-Za-z0-9'-]+)/);
|
const tokens = paraText.split(/([A-Za-z0-9'-]+)/);
|
||||||
tokens.forEach(token => {
|
tokens.forEach(token => {
|
||||||
if (/^[A-Za-z0-9'-]+$/.test(token) && token.length > 1) {
|
const isWord = /^[A-Za-z0-9'-]+$/.test(token) && token.replace(/[^A-Za-z0-9]/g, '').length >= 1;
|
||||||
|
if (isWord) {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.className = 'reader-word';
|
span.className = 'reader-word';
|
||||||
span.dataset.word = token;
|
span.dataset.word = token;
|
||||||
@@ -1951,13 +1952,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openWordPopup(term, contextSentence, targetElement) {
|
async function openWordPopup(term, contextSentence, targetElement) {
|
||||||
|
const cleanTerm = (term || '').replace(/^['"“«`\s\-_.,;:!?()[\]{}]+|['"”»`\s\-_.,;:!?()[\]{}]+$/g, '').trim();
|
||||||
|
if (!cleanTerm) return;
|
||||||
|
|
||||||
popupCurrentData = {
|
popupCurrentData = {
|
||||||
term: term,
|
term: cleanTerm,
|
||||||
contextSentence: contextSentence,
|
contextSentence: contextSentence,
|
||||||
targetElement: targetElement,
|
targetElement: targetElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
popupTerm.textContent = term;
|
popupTerm.textContent = cleanTerm;
|
||||||
popupPhonetic.textContent = '';
|
popupPhonetic.textContent = '';
|
||||||
popupTranslation.textContent = '';
|
popupTranslation.textContent = '';
|
||||||
popupContext.textContent = contextSentence ? `“${contextSentence}”` : '';
|
popupContext.textContent = contextSentence ? `“${contextSentence}”` : '';
|
||||||
@@ -1985,13 +1989,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/lookup?q=${encodeURIComponent(term)}`);
|
const resp = await fetch(`/api/lookup?q=${encodeURIComponent(cleanTerm)}`);
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
popupCurrentData.lookup = data;
|
popupCurrentData.lookup = data;
|
||||||
|
|
||||||
|
const trans = data.translation || data.translation_ru || (data.translations_list && data.translations_list.length > 0 ? data.translations_list.join(', ') : '');
|
||||||
popupPhonetic.textContent = data.phonetic || '';
|
popupPhonetic.textContent = data.phonetic || '';
|
||||||
popupTranslation.textContent = data.translation_ru || 'Перевод не найден';
|
popupTranslation.textContent = trans || 'Перевод не найден';
|
||||||
popupLoading.classList.add('hidden');
|
popupLoading.classList.add('hidden');
|
||||||
popupContent.classList.remove('hidden');
|
popupContent.classList.remove('hidden');
|
||||||
|
|
||||||
@@ -2035,7 +2040,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
term: d.term,
|
term: d.term,
|
||||||
translation: lookup.translation_ru || popupTranslation.textContent,
|
translation: lookup.translation || lookup.translation_ru || popupTranslation.textContent,
|
||||||
context_sentence: d.contextSentence || '',
|
context_sentence: d.contextSentence || '',
|
||||||
phonetic: lookup.phonetic || '',
|
phonetic: lookup.phonetic || '',
|
||||||
audio_url: lookup.audio_url || '',
|
audio_url: lookup.audio_url || '',
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
// Service Worker for Notes English PWA (Stage 2)
|
// Service Worker for Notes English PWA (Stage 2)
|
||||||
// Strategy: Cache-first for static assets, Network-first for API calls.
|
// Strategy: Cache-first for static assets, Network-first for API calls.
|
||||||
|
|
||||||
const CACHE_NAME = 'notes-v2.0';
|
const CACHE_NAME = 'notes-v2.2';
|
||||||
const OFFLINE_URL = '/';
|
const OFFLINE_URL = '/';
|
||||||
|
|
||||||
const PRECACHE_URLS = [
|
const PRECACHE_URLS = [
|
||||||
|
|||||||
Reference in New Issue
Block a user