-
Notifications
You must be signed in to change notification settings - Fork 94
Dictionary mode #519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Dictionary mode #519
Changes from 72 commits
759696e
7f47068
7993620
8ea944b
56171e3
f7b95d5
3d00114
10b6e7d
f9babf8
d9f3ada
6d04dd2
b911050
9eb28bc
089314c
8f16df2
8b78f79
eb54e41
8f18355
69718be
995bc27
10f8d23
c5ed378
556d0e7
2eb7a96
9433b2f
10c9675
ac417a0
b67cc9f
9272b91
daf68a0
f3f644c
c8f61ad
8e00263
b3e0a05
dfea9e2
38f8d4b
fc78219
a95a3ab
a436e28
767e9a4
38a4285
a7e1879
6f72ae1
5e54a6d
7f27e31
bd60d7a
ed9f07f
258d655
27167f5
f50438d
452eb61
48411fd
f6df099
778aea8
c9fa727
4334a8c
6502f9a
e47e30e
b12cd24
7fc13ac
2d518f2
a06f5f3
dff46a7
4f43589
4858506
deda844
522c7ed
749d376
e63e8e3
ef4d433
fb67b84
1330624
cea8220
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
| const LANGS_DIR = path.resolve(__dirname, '../src/components/dictionary/langs'); | ||
| const INDEX_FILE = path.resolve(__dirname, '../src/components/dictionary/index.ts'); | ||
|
|
||
| async function generateRegistry() { | ||
| const files = fs.readdirSync(LANGS_DIR).filter((f) => f.endsWith('.ts')); | ||
|
|
||
| const imports = files | ||
| .map((file) => { | ||
| const name = path.basename(file, '.ts'); | ||
| return `import { ${name}Plugin } from './langs/${name}';`; | ||
| }) | ||
| .join('\n'); | ||
|
|
||
| const registry = files | ||
| .map((file) => { | ||
| const name = path.basename(file, '.ts'); | ||
| return ` ${JSON.stringify(name)}: ${name}Plugin,`; | ||
| }) | ||
| .join('\n'); | ||
|
|
||
| const content = `import Dictionary from './Dictionary';\n${imports}\nimport { LanguagePlugin } from './types';\n\nexport const languageRegistry: Record<string, LanguagePlugin> = {\n${registry}\n};\n\nexport default Dictionary;\n`; | ||
|
|
||
| fs.writeFileSync(INDEX_FILE, content); | ||
| } | ||
|
|
||
| generateRegistry(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to auto-create an index.ts which refers to all the files in langs, but if I read it correctly, the generated file will just contain something like I understand that this build step is here to avoid having to remember to do +import { tatPlugin } from './langs/tat';
…
+ "tat": tatPlugin,on adding Tatar etc., but OTOH it adds some complexity to the build. Do we expect lots of (Does the regular localisation code also do this kind of thing?) |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| .roman-numeral { | ||
| margin-left: 0.5rem; | ||
| font-style: normal; | ||
| font-weight: 600; | ||
| font-size: 1rem; | ||
| } | ||
|
|
||
| .pos-tag { | ||
| margin-left: 0.5rem; | ||
| color: #6c757d; | ||
| font-size: 0.875rem; | ||
| } | ||
|
|
||
| .entry-divider { | ||
| border: none; | ||
| border-top: 1px solid #dee2e6; | ||
| margin: 1rem 0; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| import React, { useState, useEffect, useContext } from 'react'; | ||
| import './Word.css'; | ||
| import './CombinedWord.css'; | ||
| import Spinner from 'react-bootstrap/Spinner'; | ||
| import Dropdown from 'react-bootstrap/Dropdown'; | ||
| import Paradigm from './Paradigm'; | ||
| import { APyContext } from '../../context'; | ||
| import { useLocalization, useLocalizationPOS } from '../../util/localization'; | ||
| import { getPosTag } from '../../util/posLocalization'; | ||
| import { languageRegistry } from './index'; | ||
|
|
||
| export interface Entry { | ||
| head: string; | ||
| defs: string[]; | ||
| similarTo?: string; | ||
| extraTags?: string[]; | ||
| } | ||
|
|
||
| const toRoman = (n: number): string => { | ||
| const lookup: Record<number, string> = { | ||
| 1: 'I', | ||
| 2: 'II', | ||
| 3: 'III', | ||
| 4: 'IV', | ||
| 5: 'V', | ||
| 6: 'VI', | ||
| 7: 'VII', | ||
| 8: 'VIII', | ||
| 9: 'IX', | ||
| 10: 'X', | ||
| }; | ||
| return lookup[n] || n.toString(); | ||
| }; | ||
|
|
||
| interface EntryBlockProps { | ||
| surface: string; | ||
| entry: Entry; | ||
| lang: string; | ||
| index: number; | ||
| total: number; | ||
| onDefinitionClick: (def: string) => void; | ||
| searchWord: string; | ||
| } | ||
|
|
||
| const EntryBlock: React.FC<EntryBlockProps> = ({ | ||
| surface, | ||
| entry, | ||
| lang, | ||
| index, | ||
| total, | ||
| onDefinitionClick, | ||
| searchWord, | ||
| }) => { | ||
| const apyFetch = useContext(APyContext); | ||
| const { t } = useLocalization(); | ||
| const { locale } = useLocalizationPOS(); | ||
|
|
||
| const plugin = languageRegistry[lang] || null; | ||
| const availableModes = plugin?.getAvailableModes ? plugin.getAvailableModes(locale) : []; | ||
|
|
||
| const rawBlocks = plugin | ||
| ? plugin.addParadigms({ head: entry.head, mode: availableModes[0] || '', locale, t, apyFetch }) | ||
| : []; | ||
| const hasParadigms = Array.isArray(rawBlocks) && rawBlocks.length > 0; | ||
|
|
||
| const [expanded, setExpanded] = useState(false); | ||
| const [loadingParadigm, setLoadingParadigm] = useState(false); | ||
| const [mode, setMode] = useState<string>(availableModes[0] || ''); | ||
|
|
||
| useEffect(() => { | ||
| if (availableModes.length && !availableModes.includes(mode)) { | ||
| setMode(availableModes[0]); | ||
| } | ||
| }, [availableModes, mode, locale]); | ||
|
|
||
| const tags: string[] = []; | ||
| let m: RegExpExecArray | null; | ||
| const re = /<([^>]+)>/g; | ||
| while ((m = re.exec(entry.head))) tags.push(m[1]); | ||
| const displayTag = tags.length ? getPosTag(locale, tags.join('.')) : null; | ||
|
|
||
| const showRoman = total > 1; | ||
| const roman = showRoman ? toRoman(index + 1) : ''; | ||
|
|
||
| const cleanSurface = surface; | ||
| const cleanDefs = entry.defs.map((d) => d.replace(/<[^>]+>/g, '')); | ||
|
|
||
| const cleanedSearch = searchWord.replace(/<[^>]+>/g, ''); | ||
| const nonExact = cleanSurface !== cleanedSearch; | ||
|
|
||
| const extraTagsString = entry.extraTags && entry.extraTags.length ? entry.extraTags.join('') : ''; | ||
| const extraTokens = Array.from(extraTagsString.matchAll(/<([^>]+)>/g)).map((mm) => mm[1]); | ||
|
|
||
| const segmentMorphLabels = (tokens: string[]): string | null => { | ||
| let i = 0; | ||
| const labels: string[] = []; | ||
| while (i < tokens.length) { | ||
| let foundLabel: string | null = null; | ||
| let foundLen = 0; | ||
| for (let j = tokens.length; j > i; j--) { | ||
| const key = tokens.slice(i, j).join('.'); | ||
| const lbl = getPosTag(locale, key); | ||
| if (lbl && lbl !== key) { | ||
| foundLabel = lbl; | ||
| foundLen = j - i; | ||
| break; | ||
| } | ||
| } | ||
| if (foundLabel) { | ||
| labels.push(foundLabel); | ||
| i += foundLen; | ||
| } else { | ||
| i += 1; | ||
| } | ||
| } | ||
| return labels.length ? labels.join(' ') : null; | ||
| }; | ||
|
|
||
| const morphLabel = segmentMorphLabels(extraTokens); | ||
| const extraDisplay = morphLabel || extraTagsString; | ||
|
|
||
| const handleToggle = () => { | ||
| if (!expanded) { | ||
| setExpanded(true); | ||
| setLoadingParadigm(true); | ||
| } else { | ||
| setExpanded(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="pos-block"> | ||
| <div className="word-header"> | ||
| <span className="word-text"> | ||
| {cleanSurface} | ||
| {showRoman && <span className="roman-numeral">{roman}</span>} | ||
| {displayTag && <span className="pos-tag">({displayTag})</span>} | ||
| </span> | ||
| </div> | ||
| <ol className="word-definitions"> | ||
| {cleanDefs.map((rawDef, i) => { | ||
| const def = rawDef; | ||
| return ( | ||
| <li key={i} className="definition-item" onClick={() => onDefinitionClick(def)}> | ||
| {def} | ||
| </li> | ||
| ); | ||
| })} | ||
| </ol> | ||
| {entry.similarTo && ( | ||
| <div className="extra-tag-info small text-muted mt-1"> | ||
| <small> | ||
| <em>{t('Similar_To')}</em> {entry.similarTo} | ||
| </small> | ||
| </div> | ||
| )} | ||
| {nonExact && extraTagsString && ( | ||
| <div className="extra-tag-info small text-muted mt-1"> | ||
| {extraDisplay}: {cleanedSearch} | ||
| </div> | ||
| )} | ||
| {hasParadigms && ( | ||
| <div className="expand-controls mt-1"> | ||
| <button type="button" className="expand-button" onClick={handleToggle} disabled={loadingParadigm}> | ||
| {loadingParadigm ? ( | ||
| <> | ||
| <Spinner animation="border" size="sm" className="me-2" /> | ||
| {t('Expand_Paradigms')} | ||
| </> | ||
| ) : expanded ? ( | ||
| t('Collapse_Paradigms') | ||
| ) : ( | ||
| t('Expand_Paradigms') | ||
| )} | ||
| </button> | ||
| {expanded && availableModes.length > 1 && ( | ||
| <Dropdown onSelect={(k) => typeof k === 'string' && setMode(k)}> | ||
| <Dropdown.Toggle id="mode-dropdown" className="expand-button"> | ||
| {mode} | ||
| </Dropdown.Toggle> | ||
| <Dropdown.Menu> | ||
| {availableModes.map((mk) => ( | ||
| <Dropdown.Item key={mk} eventKey={mk}> | ||
| {mk} | ||
| </Dropdown.Item> | ||
| ))} | ||
| </Dropdown.Menu> | ||
| </Dropdown> | ||
| )} | ||
| </div> | ||
| )} | ||
| {hasParadigms && expanded && ( | ||
| <div className="word-paradigm"> | ||
| <Paradigm head={entry.head} lang={lang} mode={mode} onLoaded={() => setLoadingParadigm(false)} /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| interface CombinedWordProps { | ||
| surface: string; | ||
| entries: Entry[]; | ||
| lang: string; | ||
| onDefinitionClick: (def: string) => void; | ||
| searchWord: string; | ||
| } | ||
|
|
||
| const CombinedWord: React.FC<CombinedWordProps> = ({ surface, entries, lang, onDefinitionClick, searchWord }) => ( | ||
| <div className="word-card"> | ||
| {entries.map((e, idx) => ( | ||
| <React.Fragment key={idx}> | ||
| <EntryBlock | ||
| surface={surface} | ||
| entry={e} | ||
| lang={lang} | ||
| index={idx} | ||
| total={entries.length} | ||
| onDefinitionClick={onDefinitionClick} | ||
| searchWord={searchWord} | ||
| /> | ||
| {idx < entries.length - 1 && <div className="entry-divider" />} | ||
| </React.Fragment> | ||
| ))} | ||
| </div> | ||
| ); | ||
|
|
||
| export default CombinedWord; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The config can't be merged as-is. Don't change the default URL, mode, name, or color.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vykliuk, yeah, revert your changes to the config file in this branch/PR, and just keep them locally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was apparently entirely inadvertent. I've reverted it at @vykliuk's request.