|
11 | 11 |
|
12 | 12 | const MAX_RESULTS = 100; |
13 | 13 |
|
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'; |
18 | 38 | const results = []; |
19 | 39 | 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]); |
24 | 41 | } |
25 | 42 | return results; |
26 | 43 | } |
27 | 44 |
|
28 | 45 | // ---------- 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 | + } |
30 | 52 |
|
31 | 53 | function levenshtein(a, b){ |
32 | 54 | a = normalize(a); b = normalize(b); |
|
57 | 79 | return 1 - dist / maxLen; |
58 | 80 | } |
59 | 81 |
|
60 | | - function findGlossaryMatch(term){ |
| 82 | + function findGlossaryMatch(term, field){ |
61 | 83 | const norm = normalize(term); |
62 | 84 | // exact match first (handles multi-variant "en" keys and comma lists) |
63 | 85 | 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 }; |
65 | 87 | } |
66 | 88 | // substring / contains match |
67 | 89 | let best = null; |
68 | 90 | for (const entry of GLOSSARY){ |
69 | | - const enNorm = normalize(entry.en); |
| 91 | + const enNorm = normalize(entry[field]); |
70 | 92 | if (enNorm.includes(norm) || norm.includes(enNorm)){ |
71 | 93 | const score = Math.min(norm.length, enNorm.length) / Math.max(norm.length, enNorm.length); |
72 | 94 | if (!best || score > best.score) best = { entry, score }; |
|
76 | 98 | // fuzzy fallback (semantic-similarity stand-in) |
77 | 99 | best = null; |
78 | 100 | for (const entry of GLOSSARY){ |
79 | | - const score = similarity(term, entry.en); |
| 101 | + const score = similarity(term, entry[field]); |
80 | 102 | if (score > 0.72 && (!best || score > best.score)) best = { entry, score }; |
81 | 103 | } |
82 | 104 | return best; |
|
90 | 112 | const PAGE_SIZE = 10; |
91 | 113 | let currentPage = 1; |
92 | 114 | let currentResults = []; |
93 | | - let currentTerm = ''; |
| 115 | + let currentMatcher = null; |
| 116 | + let currentFa = false; |
94 | 117 |
|
95 | 118 | function escapeHtml(s){ |
96 | 119 | return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); |
97 | 120 | } |
98 | 121 |
|
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)); |
105 | 127 | } |
106 | 128 |
|
107 | 129 | function githubContributingDocumentLink(kind, term){ |
108 | 130 | 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"; |
110 | 132 | } 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"; |
112 | 134 | } |
113 | 135 | } |
114 | 136 |
|
|
152 | 174 | for (const r of pageItems){ |
153 | 175 | html += `<div class="result-row"> |
154 | 176 | <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> |
156 | 178 | <div class="result-source">${escapeHtml(r.file)}:${r.line}</div> |
157 | 179 | </div> |
158 | | - <div class="result-fa">${escapeHtml(r.msgstr)}</div> |
| 180 | + <div class="result-fa">${currentFa ? highlight(r.msgstr, currentMatcher) : escapeHtml(r.msgstr)}</div> |
159 | 181 | </div>`; |
160 | 182 | } |
161 | 183 | html += '</div>'; |
|
178 | 200 | function doSearch(term){ |
179 | 201 | term = term.trim(); |
180 | 202 | if (!term) return; |
181 | | - currentTerm = term; |
| 203 | + currentFa = isPersian(term); |
| 204 | + currentMatcher = buildMatcher(term, currentFa); |
182 | 205 | currentPage = 1; |
183 | 206 |
|
184 | 207 | hero.classList.add('compact'); |
185 | 208 | resultsArea.classList.add('visible'); |
186 | 209 |
|
187 | | - const match = findGlossaryMatch(term); |
188 | | - currentResults = searchCorpus(term); |
| 210 | + const match = findGlossaryMatch(term, currentFa ? 'fa' : 'en'); |
| 211 | + currentResults = searchCorpus(currentMatcher, currentFa); |
189 | 212 |
|
190 | 213 | let html = ''; |
191 | 214 |
|
192 | 215 | if (match){ |
193 | 216 | html += `<div class="glossary-card"> |
194 | 217 | <div class="glossary-card-label">واژههای مشابه درخواست شما که در واژهنامه یافت شدند:</div> |
195 | 218 | <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> |
197 | 220 | <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> |
199 | 222 | </div> |
200 | 223 | ${match.score < 1 ? `<div class="glossary-match-score">مطابقت تقریبی (${Math.round(match.score*100)}٪)</div>` : ''} |
201 | 224 | </div>`; |
|
0 commit comments