Skip to content

Commit f0f38f8

Browse files
committed
Add Persian to English search support to glossary-searcher
1 parent 518a3b4 commit f0f38f8

2 files changed

Lines changed: 54 additions & 31 deletions

File tree

‎glossary-searcher/site/app.js‎

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,44 @@
1111

1212
const MAX_RESULTS = 100;
1313

14-
// Searches the real corpus for entries whose msgid contains the term
15-
// (case-insensitive). Returns up to MAX_RESULTS matches.
16-
function searchCorpus(term){
17-
const needle = term.toLowerCase();
14+
// Persian text contains Arabic-script letters; if the query has any, we
15+
// search Persian -> English, otherwise English -> Persian.
16+
function isPersian(term){ return /[\u0600-\u06FF]/.test(term); }
17+
18+
function escapeRegex(s){ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
19+
20+
// Builds a case-insensitive matcher. For Persian it tolerates the common
21+
// variants: Arabic vs Persian ya/kaf, optional ZWNJ, optional diacritics.
22+
function buildMatcher(term, fa){
23+
if (!fa) return new RegExp(escapeRegex(term), 'i');
24+
const optional = '[\\u200c\\u064B-\\u065F\\u0640]*';
25+
const src = Array.from(term.replace(/[\u200c]/g, '')).map(ch => {
26+
if (ch === 'ی' || ch === 'ي') return '[یي]';
27+
if (ch === 'ک' || ch === 'ك') return '[کك]';
28+
if (/\s/.test(ch)) return '[\\s\\u200c]+';
29+
return escapeRegex(ch);
30+
}).join(optional);
31+
return new RegExp(src, 'i');
32+
}
33+
34+
// Returns up to MAX_RESULTS corpus entries whose msgid (EN) or msgstr (FA)
35+
// matches.
36+
function searchCorpus(re, fa){
37+
const field = fa ? 'msgstr' : 'msgid';
1838
const results = [];
1939
for (let i = 0; i < CORPUS.length && results.length < MAX_RESULTS; i++){
20-
const entry = CORPUS[i];
21-
if (entry.msgid.toLowerCase().includes(needle)){
22-
results.push(entry);
23-
}
40+
if (re.test(CORPUS[i][field])) results.push(CORPUS[i]);
2441
}
2542
return results;
2643
}
2744

2845
// ---------- Simple fuzzy matching for glossary lookup ----------
29-
function normalize(s){ return s.toLowerCase().trim(); }
46+
function normalize(s){
47+
return s.toLowerCase()
48+
.replace(/ي/g,'ی').replace(/ك/g,'ک')
49+
.replace(/[\u200c\u064B-\u065F\u0640]/g,'')
50+
.trim();
51+
}
3052

3153
function levenshtein(a, b){
3254
a = normalize(a); b = normalize(b);
@@ -57,16 +79,16 @@
5779
return 1 - dist / maxLen;
5880
}
5981

60-
function findGlossaryMatch(term){
82+
function findGlossaryMatch(term, field){
6183
const norm = normalize(term);
6284
// exact match first (handles multi-variant "en" keys and comma lists)
6385
for (const entry of GLOSSARY){
64-
if (normalize(entry.en) === norm) return { entry, score: 1 };
86+
if (normalize(entry[field]) === norm) return { entry, score: 1 };
6587
}
6688
// substring / contains match
6789
let best = null;
6890
for (const entry of GLOSSARY){
69-
const enNorm = normalize(entry.en);
91+
const enNorm = normalize(entry[field]);
7092
if (enNorm.includes(norm) || norm.includes(enNorm)){
7193
const score = Math.min(norm.length, enNorm.length) / Math.max(norm.length, enNorm.length);
7294
if (!best || score > best.score) best = { entry, score };
@@ -76,7 +98,7 @@
7698
// fuzzy fallback (semantic-similarity stand-in)
7799
best = null;
78100
for (const entry of GLOSSARY){
79-
const score = similarity(term, entry.en);
101+
const score = similarity(term, entry[field]);
80102
if (score > 0.72 && (!best || score > best.score)) best = { entry, score };
81103
}
82104
return best;
@@ -90,25 +112,25 @@
90112
const PAGE_SIZE = 10;
91113
let currentPage = 1;
92114
let currentResults = [];
93-
let currentTerm = '';
115+
let currentMatcher = null;
116+
let currentFa = false;
94117

95118
function escapeHtml(s){
96119
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
97120
}
98121

99-
function highlight(text, term){
100-
const escaped = escapeHtml(text);
101-
if (!term) return escaped;
102-
const idx = escaped.toLowerCase().indexOf(term.toLowerCase());
103-
if (idx === -1) return escaped;
104-
return escaped.slice(0, idx) + '<mark>' + escaped.slice(idx, idx+term.length) + '</mark>' + escaped.slice(idx+term.length);
122+
function highlight(text, re){
123+
const m = re && re.exec(text);
124+
if (!m) return escapeHtml(text);
125+
const i = m.index, j = i + m[0].length;
126+
return escapeHtml(text.slice(0,i)) + '<mark>' + escapeHtml(text.slice(i,j)) + '</mark>' + escapeHtml(text.slice(j));
105127
}
106128

107129
function githubContributingDocumentLink(kind, term){
108130
if (kind === 'report'){ // translation bugs
109-
return "https://github.com/python/python-docs-fa/blob/3.15/CONTRIBUTING.md#%DA%AF%D8%B2%D8%A7%D8%B1%D8%B4-%D8%A7%D8%B4%DA%A9%D8%A7%D9%84-%D8%AF%D8%B1-%D8%AA%D8%B1%D8%AC%D9%85%D9%87";
131+
return "https://github.com/python/python-docs-fa/blob/3.14/CONTRIBUTING.md#%DA%AF%D8%B2%D8%A7%D8%B1%D8%B4-%D8%A7%D8%B4%DA%A9%D8%A7%D9%84-%D8%AF%D8%B1-%D8%AA%D8%B1%D8%AC%D9%85%D9%87";
110132
} else {
111-
return "https://github.com/python/python-docs-fa/blob/3.15/CONTRIBUTING.md#%D8%A7%D9%81%D8%B2%D9%88%D8%AF%D9%86-%D9%88%D8%A7%DA%98%D9%87-%DB%8C%D8%A7-%D8%A7%D8%B5%D8%B7%D9%84%D8%A7%D8%AD-%D8%AC%D8%AF%DB%8C%D8%AF-%D8%A8%D9%87-%D9%88%D8%A7%DA%98%D9%87%E2%80%8C%D9%86%D8%A7%D9%85%D9%87";
133+
return "https://github.com/python/python-docs-fa/blob/3.14/CONTRIBUTING.md#%D8%A7%D9%81%D8%B2%D9%88%D8%AF%D9%86-%D9%88%D8%A7%DA%98%D9%87-%DB%8C%D8%A7-%D8%A7%D8%B5%D8%B7%D9%84%D8%A7%D8%AD-%D8%AC%D8%AF%DB%8C%D8%AF-%D8%A8%D9%87-%D9%88%D8%A7%DA%98%D9%87%E2%80%8C%D9%86%D8%A7%D9%85%D9%87";
112134
}
113135
}
114136

@@ -152,10 +174,10 @@
152174
for (const r of pageItems){
153175
html += `<div class="result-row">
154176
<div>
155-
<div class="result-en">${highlight(r.msgid, currentTerm)}</div>
177+
<div class="result-en">${currentFa ? escapeHtml(r.msgid) : highlight(r.msgid, currentMatcher)}</div>
156178
<div class="result-source">${escapeHtml(r.file)}:${r.line}</div>
157179
</div>
158-
<div class="result-fa">${escapeHtml(r.msgstr)}</div>
180+
<div class="result-fa">${currentFa ? highlight(r.msgstr, currentMatcher) : escapeHtml(r.msgstr)}</div>
159181
</div>`;
160182
}
161183
html += '</div>';
@@ -178,24 +200,25 @@
178200
function doSearch(term){
179201
term = term.trim();
180202
if (!term) return;
181-
currentTerm = term;
203+
currentFa = isPersian(term);
204+
currentMatcher = buildMatcher(term, currentFa);
182205
currentPage = 1;
183206

184207
hero.classList.add('compact');
185208
resultsArea.classList.add('visible');
186209

187-
const match = findGlossaryMatch(term);
188-
currentResults = searchCorpus(term);
210+
const match = findGlossaryMatch(term, currentFa ? 'fa' : 'en');
211+
currentResults = searchCorpus(currentMatcher, currentFa);
189212

190213
let html = '';
191214

192215
if (match){
193216
html += `<div class="glossary-card">
194217
<div class="glossary-card-label">واژه‌های مشابه درخواست شما که در واژه‌نامه یافت شدند:</div>
195218
<div class="glossary-card-term">
196-
<span class="glossary-en">${escapeHtml(match.entry.en)}</span>
219+
<span class="glossary-en">${escapeHtml(currentFa ? match.entry.fa : match.entry.en)}</span>
197220
<span class="glossary-arrow">→</span>
198-
<span class="glossary-fa">${escapeHtml(match.entry.fa)}</span>
221+
<span class="glossary-fa">${escapeHtml(currentFa ? match.entry.en : match.entry.fa)}</span>
199222
</div>
200223
${match.score < 1 ? `<div class="glossary-match-score">مطابقت تقریبی (${Math.round(match.score*100)}٪)</div>` : ''}
201224
</div>`;

‎glossary-searcher/site/index.html‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@
2929
<div class="hero" id="hero">
3030
<img src="assets/logo.svg" alt="Python" class="py-logo">
3131
<h1 class="hero-title">واژه‌یاب پایتون</h1>
32-
<p class="hero-sub">ترجمه‌ی واژگان انگلیسی را در مستندات فارسی پایتون پیدا کنید</p>
32+
<p class="hero-sub">ترجمه‌ی واژگان انگلیسی را در مستندات فارسی پایتون پیدا کنید، یا از فارسی به انگلیسی جست‌وجو کنید.</p>
3333
<div class="search-wrap">
3434
<div class="search-box">
3535
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
36-
<input type="text" id="search-input" placeholder="مثلاً decorator یا coroutine" autocomplete="off" spellcheck="false">
36+
<input type="text" id="search-input" placeholder="مثلاً decorator یا تابع" autocomplete="off" spellcheck="false">
3737
</div>
3838
<p class="search-hint">جست‌وجو در واژه‌نامه رسمی و پیکره‌ی ترجمه‌ی مستندات پایتون</p>
3939
</div>

0 commit comments

Comments
 (0)