diff --git a/frontend/viewer/src/lib/components/dictionary/DictionaryEntry.svelte b/frontend/viewer/src/lib/components/dictionary/DictionaryEntry.svelte index ed35db80fc..cc45467a79 100644 --- a/frontend/viewer/src/lib/components/dictionary/DictionaryEntry.svelte +++ b/frontend/viewer/src/lib/components/dictionary/DictionaryEntry.svelte @@ -15,6 +15,7 @@ headwordClass = '', highlightSenseId = undefined, hideExamples = false, + inline = false, ...restProps }: HTMLAttributes & { entry: IEntry; @@ -24,6 +25,8 @@ headwordClass?: string; highlightSenseId?: string; hideExamples?: boolean; + /** Render senses as one flowing line (no line break per sense) — for compact previews */ + inline?: boolean; } = $props(); $effect(() => { @@ -104,7 +107,12 @@ {#each senses as sense, i (sense.id)} {#if senses.length > 1} -
+ {#if inline} + + {' '} + {:else} +
+ {/if} {/if} {#if senses.length > 1} diff --git a/frontend/viewer/src/lib/entry-editor/DuplicateCheck.svelte b/frontend/viewer/src/lib/entry-editor/DuplicateCheck.svelte new file mode 100644 index 0000000000..106f8428b9 --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/DuplicateCheck.svelte @@ -0,0 +1,294 @@ + + + + +
+ {#if !matches?.length} + {#if (duplicatesResource.loading && hasQueries) || matches} +
+ {#if duplicatesResource.loading} + + {pt($t`Checking for similar entries…`, $t`Checking for similar words…`, viewService.currentView)} + {:else} + + {pt($t`No similar entries found`, $t`Looks like a new word`, viewService.currentView)} + {/if} +
+ {/if} + {:else} + userToggled = true} + class="rounded-md border {duplicateTintClass(hasExactWordMatch)}" + > + + {#if hasExactWordMatch} + + {:else} + + {/if} + + {summaryMessage} + {#if !expanded && previewHeadwords} + — {previewHeadwords} + {/if} + + {#if duplicatesResource.loading} + + {/if} + {matches.length}{duplicatesResource.current?.capped ? '+' : ''} + + + +
    + {#each displayedMatches as match (match.entry.id)} + {@const badge = kindLabel(match)} + {@const isExpanded = expandedEntryId === match.entry.id} +
  • + + {#if isExpanded} +
    + {#if canAddSense && (match.kind === 'same-word' || match.kind === 'similar-word')} + {@const addSenseLabel = pt($t`Add sense`, $t`Add meaning`, viewService.currentView)} + {@const addSenseHint = pt($t`Add sense to this entry`, $t`Add meaning to this word`, viewService.currentView)} + + {/if} + +
    + {/if} +
  • + {/each} + {#if matches.length > displayedMatches.length} + {@const remainingEntries = matches.length - displayedMatches.length} +
  • + +
  • + {/if} +
+
+
+ {/if} +
diff --git a/frontend/viewer/src/lib/entry-editor/DuplicateSummaryPill.svelte b/frontend/viewer/src/lib/entry-editor/DuplicateSummaryPill.svelte new file mode 100644 index 0000000000..a98ba537f7 --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/DuplicateSummaryPill.svelte @@ -0,0 +1,55 @@ + + + + +
+
+ + +
+
diff --git a/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte b/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte index 32f6b7515a..3adee62060 100644 --- a/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte +++ b/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte @@ -7,7 +7,7 @@ {#if open} @@ -178,11 +212,16 @@ {/snippet} - + + {pt($t`New Entry`, $t`New Word`, viewService.currentView)} -
+ +
+
+ open = false} /> +
+ {#if duplicateSummary && !duplicateWidgetVisible && !pillDismissed} + + {/if}
{#if errors.length}
@@ -221,7 +273,7 @@ {/if} - diff --git a/frontend/viewer/src/lib/entry-editor/duplicate-check.test.ts b/frontend/viewer/src/lib/entry-editor/duplicate-check.test.ts new file mode 100644 index 0000000000..48a87a246d --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/duplicate-check.test.ts @@ -0,0 +1,306 @@ +import {describe, expect, it} from 'vitest'; +import type {IEntry} from '$lib/dotnet-types'; +import { + classifyDuplicates, + duplicateQueries, + hasDiacritics, + mergeSearchResults, + normalizeForCompare, + stripMorphTokens, + type DuplicateQueries, +} from './duplicate-check'; + +const vernWs = ['seh', 'por']; +const analysisWs = ['en', 'fr']; +// canonical suffix/prefix morph-token shapes (CanonicalMorphTypes) +const morphTypes = [{prefix: '-', postfix: undefined}, {prefix: undefined, postfix: '-'}]; + +function makeEntry(partial: Partial): IEntry { + return { + id: crypto.randomUUID(), + lexemeForm: {}, + citationForm: {}, + senses: [], + ...partial, + } as IEntry; +} + +function withGloss(lexeme: string, gloss: string): IEntry { + return makeEntry({ + lexemeForm: {seh: lexeme}, + senses: [{gloss: {en: gloss}} as unknown as IEntry['senses'][number]], + }); +} + +function vernQueries(...texts: string[]): DuplicateQueries { + return {vernacular: texts.map(text => ({text, wsId: 'seh'})), analysis: []}; +} + +describe('normalizeForCompare', () => { + it('ignores case and accents by default', () => { + expect(normalizeForCompare('Ñumbá ')).toBe('numba'); + expect(normalizeForCompare('CAFÉ')).toBe('cafe'); + }); + + it('keeps diacritics when asked, still folding case', () => { + expect(normalizeForCompare('CAFÉ', true)).toBe('café'.normalize('NFD')); + expect(normalizeForCompare('CAFÉ', true)).not.toBe('cafe'); + }); + + it('folds case invariantly, not by host locale', () => { + // must never produce Turkish dotless ı for ASCII I + expect(normalizeForCompare('IZGARA')).toBe('izgara'); + }); +}); + +describe('hasDiacritics', () => { + it('detects combining marks in composed and decomposed input', () => { + expect(hasDiacritics('café')).toBe(true); + expect(hasDiacritics('café')).toBe(true); + expect(hasDiacritics('cafe')).toBe(false); + }); +}); + +describe('stripMorphTokens', () => { + it('strips a leading token', () => { + expect(stripMorphTokens('-aji', morphTypes)).toBe('aji'); + }); + + it('strips a trailing token', () => { + expect(stripMorphTokens('a-', morphTypes)).toBe('a'); + }); + + it('prefers the type matching a leading token over a trailing one', () => { + // '-a-' matches both shapes; leading-token match scores higher (mirrors backend scoring) + expect(stripMorphTokens('-a-', [{prefix: '-', postfix: '-'}])).toBe('a'); + }); + + it('leaves untokenized input alone', () => { + expect(stripMorphTokens('aji', morphTypes)).toBe('aji'); + }); +}); + +describe('duplicateQueries', () => { + it('collects distinct vernacular texts with their writing system, and gloss texts', () => { + const queries = duplicateQueries( + {lexemeForm: {seh: 'nyumba', por: 'casa'}, citationForm: {seh: 'nyumba'}}, + {gloss: {en: 'house', fr: ''}}, + vernWs, + analysisWs, + ); + expect(queries.vernacular).toEqual([{text: 'nyumba', wsId: 'seh'}, {text: 'casa', wsId: 'por'}]); + expect(queries.analysis).toEqual(['house']); + }); + + it('skips blank and too-short values', () => { + const queries = duplicateQueries( + {lexemeForm: {seh: ' n '}, citationForm: {}}, + {gloss: {en: ' '}}, + vernWs, + analysisWs, + ); + expect(queries.vernacular).toEqual([]); + expect(queries.analysis).toEqual([]); + }); + + it('accepts a value exactly at the length threshold', () => { + const queries = duplicateQueries( + {lexemeForm: {seh: 'ba'}, citationForm: {}}, + undefined, + vernWs, + analysisWs, + ); + expect(queries.vernacular).toEqual([{text: 'ba', wsId: 'seh'}]); + }); + + it('measures the length threshold on the normalized text', () => { + // 'e' + combining acute is 2 chars raw but 1 char once marks are stripped + const queries = duplicateQueries( + {lexemeForm: {seh: 'é'}, citationForm: {}}, + undefined, + vernWs, + analysisWs, + ); + expect(queries.vernacular).toEqual([]); + }); + + it('dedupes values that only differ by case or accents', () => { + const queries = duplicateQueries( + {lexemeForm: {seh: 'café'}, citationForm: {seh: 'Cafe'}}, + undefined, + vernWs, + analysisWs, + ); + expect(queries.vernacular).toEqual([{text: 'café', wsId: 'seh'}]); + }); + + it('keeps a gloss query even when the same text was typed as a vernacular value', () => { + // loanword case: lexeme 'radio' glossed 'radio' — the gloss query must survive so + // same-meaning matches on other entries still classify + const queries = duplicateQueries( + {lexemeForm: {seh: 'radio'}, citationForm: {}}, + {gloss: {en: 'radio'}}, + vernWs, + analysisWs, + ); + expect(queries.vernacular).toEqual([{text: 'radio', wsId: 'seh'}]); + expect(queries.analysis).toEqual(['radio']); + }); + + it('ignores values in writing systems outside the given lists', () => { + const queries = duplicateQueries( + {lexemeForm: {'seh-Zxxx-x-audio': 'clip.wav', seh: 'nyumba'}, citationForm: {}}, + undefined, + vernWs, + analysisWs, + ); + expect(queries.vernacular).toEqual([{text: 'nyumba', wsId: 'seh'}]); + }); +}); + +describe('mergeSearchResults', () => { + it('dedupes entries matched by multiple queries, preserving first-seen order', () => { + const a = makeEntry({}); + const b = makeEntry({}); + const c = makeEntry({}); + expect(mergeSearchResults([[a, b], [b, c, a]])).toEqual([a, b, c]); + }); +}); + +describe('classifyDuplicates', () => { + const queries: DuplicateQueries = {vernacular: [{text: 'nyumba', wsId: 'seh'}], analysis: ['house']}; + + it('classifies exact headword matches as same-word, even via citation form or other ws', () => { + const byLexeme = makeEntry({lexemeForm: {seh: 'Nyumbá'}}); + const byCitation = makeEntry({citationForm: {por: 'nyumba'}}); + const result = classifyDuplicates([byLexeme, byCitation], queries, vernWs, analysisWs); + expect(result.map(m => m.kind)).toEqual(['same-word', 'same-word']); + }); + + it('classifies partial headword overlap (either direction) as similar-word', () => { + const superstring = makeEntry({lexemeForm: {seh: 'nyumbazi'}}); + const substring = makeEntry({lexemeForm: {seh: 'yumba'}}); + const result = classifyDuplicates([superstring, substring], queries, vernWs, analysisWs); + expect(result.map(m => m.kind)).toEqual(['similar-word', 'similar-word']); + }); + + it('classifies mid-word containment as similar-word, not just starts-with', () => { + // real user story: typing "liman" must surface the existing entry "naliman" + const buried = makeEntry({lexemeForm: {seh: 'kumanyumba'}}); + expect(classifyDuplicates([buried], queries, vernWs, analysisWs)[0]?.kind).toBe('similar-word'); + }); + + it('drops containment matches just past the length-delta cap', () => { + // real user story: typing "uz" must not surface every word containing it; + // 'uzembez' is delta 5 (kept, exactly at the cap) and 'uzembeza' is delta 6 (dropped) + const atCap = makeEntry({lexemeForm: {seh: 'uzembez'}}); + const pastCap = makeEntry({lexemeForm: {seh: 'uzembeza'}}); + const result = classifyDuplicates([atCap, pastCap], vernQueries('uz'), vernWs, analysisWs); + expect(result).toEqual([{entry: atCap, kind: 'similar-word'}]); + }); + + it('sorts similar words closest in length first', () => { + const far = makeEntry({lexemeForm: {seh: 'kumanyumba'}}); + const near = makeEntry({lexemeForm: {seh: 'nyumbazi'}}); + const result = classifyDuplicates([far, near], queries, vernWs, analysisWs); + expect(result.map(m => m.entry.id)).toEqual([near.id, far.id]); + }); + + it('ranks an entry by its closest form when several forms are similar', () => { + // closeViaCitation: lexeme is delta 4 but citation is delta 1 — the citation should rank it + const closeViaCitation = makeEntry({lexemeForm: {seh: 'kunyumbaza'}, citationForm: {seh: 'nyumbaz'}}); + const middling = makeEntry({lexemeForm: {seh: 'nyumbazi'}}); + const result = classifyDuplicates([middling, closeViaCitation], queries, vernWs, analysisWs); + expect(result.map(m => m.entry.id)).toEqual([closeViaCitation.id, middling.id]); + }); + + it('treats a typed morph token like the backend: "-aji" is the same word as suffix entry "aji"', () => { + const suffixEntry = makeEntry({lexemeForm: {seh: 'aji'}}); + const result = classifyDuplicates([suffixEntry], vernQueries('-aji'), vernWs, analysisWs, morphTypes); + expect(result[0].kind).toBe('same-word'); + }); + + it('strips morph tokens for lexeme forms but keeps them for citation forms (backend parity)', () => { + const byLexeme = makeEntry({lexemeForm: {seh: 'aji'}}); + const byCitation = makeEntry({citationForm: {seh: 'aji'}}); + const result = classifyDuplicates([byLexeme, byCitation], vernQueries('-aji'), vernWs, analysisWs, morphTypes); + const kindById = new Map(result.map(m => [m.entry.id, m.kind])); + expect(kindById.get(byLexeme.id)).toBe('same-word'); + // citation form 'aji' is compared against the un-stripped '-aji', so it is not the same word + expect(kindById.get(byCitation.id)).not.toBe('same-word'); + }); + + it('is accent-insensitive only when the typed text has no diacritics (backend parity)', () => { + const plain = makeEntry({lexemeForm: {seh: 'cafe'}}); + const accented = makeEntry({lexemeForm: {seh: 'café'}}); + // typed without diacritics -> accents ignored -> both are the same word + expect(classifyDuplicates([plain, accented], vernQueries('cafe'), vernWs, analysisWs).map(m => m.kind)) + .toEqual(['same-word', 'same-word']); + // typed with diacritics -> accents significant -> only the accented entry matches exactly + const result = classifyDuplicates([accented, plain], vernQueries('café'), vernWs, analysisWs); + expect(result[0]).toEqual({entry: accented, kind: 'same-word', field: 'headword'}); + expect(result[1].kind).not.toBe('same-word'); + }); + + it('attributes a same-word match to the field that hit', () => { + // lexeme 'fuz' + citation 'fuza': the headword shown is 'fuza', so a match on the + // typed 'fuz' is the same entry but NOT the same headword + const viaLexemeOnly = makeEntry({lexemeForm: {seh: 'fuz'}, citationForm: {seh: 'fuza'}}); + const viaCitation = makeEntry({lexemeForm: {seh: 'fu'}, citationForm: {seh: 'fuz'}}); + const lexemeIsHeadword = makeEntry({lexemeForm: {seh: 'fuz'}}); + const result = classifyDuplicates([viaLexemeOnly, viaCitation, lexemeIsHeadword], vernQueries('fuz'), vernWs, analysisWs); + const fieldById = new Map(result.map(m => [m.entry.id, m.field])); + expect(fieldById.get(viaLexemeOnly.id)).toBe('lexeme'); + expect(fieldById.get(viaCitation.id)).toBe('headword'); + expect(fieldById.get(lexemeIsHeadword.id)).toBe('headword'); + }); + + it('prefers a citation-form hit over an earlier lexeme-only hit across queries', () => { + // one entry, two typed values: 'fuz' hits only the lexeme form, 'fuza' hits the + // citation form — the citation hit must win the field attribution + const entry = makeEntry({lexemeForm: {seh: 'fuz'}, citationForm: {seh: 'fuza'}}); + const result = classifyDuplicates([entry], vernQueries('fuz', 'fuza'), vernWs, analysisWs); + expect(result[0]).toEqual({entry, kind: 'same-word', field: 'headword'}); + }); + + it('classifies gloss overlap as same-meaning', () => { + const entry = withGloss('cabana', 'house'); + expect(classifyDuplicates([entry], queries, vernWs, analysisWs)[0].kind).toBe('same-meaning'); + }); + + it('classifies partial gloss containment in either direction as same-meaning', () => { + const glossContainsQuery = withGloss('cabana', 'houseboat'); + const queryContainsGloss = withGloss('cabana', 'use'); + const result = classifyDuplicates([glossContainsQuery, queryContainsGloss], queries, vernWs, analysisWs); + expect(result.map(m => m.kind)).toEqual(['same-meaning', 'same-meaning']); + }); + + it('drops candidates that overlap in neither headword nor gloss', () => { + // the full-text search can return an entry via a field we don't classify (e.g. definition); + // it should be dropped, not shown as a vague match + const entry = withGloss('cabana', 'dwelling'); + expect(classifyDuplicates([entry], queries, vernWs, analysisWs)).toEqual([]); + }); + + it('drops a candidate whose gloss equals a typed vernacular value (cross-field coincidence)', () => { + // typing lexeme 'nyumba' must not surface an entry merely because its gloss is 'nyumba' + const entry = withGloss('cabana', 'nyumba'); + expect(classifyDuplicates([entry], vernQueries('nyumba'), vernWs, analysisWs)).toEqual([]); + }); + + it('sorts word matches above meaning matches, preserving relevance order within a kind', () => { + const meaning = withGloss('cabana', 'house'); + const similarA = makeEntry({lexemeForm: {seh: 'nyumbazi'}}); + const similarB = makeEntry({lexemeForm: {seh: 'manyumba'}}); + const exact = makeEntry({lexemeForm: {seh: 'nyumba'}}); + const result = classifyDuplicates([meaning, similarA, similarB, exact], queries, vernWs, analysisWs); + expect(result.map(m => m.entry.id)).toEqual([exact.id, similarA.id, similarB.id, meaning.id]); + }); + + it('never reports a headword match when no vernacular text was typed', () => { + // gloss-only query: an entry matched purely by headword is a cross-field coincidence and dropped + const entry = makeEntry({lexemeForm: {seh: 'nyumba'}}); + const result = classifyDuplicates([entry], {vernacular: [], analysis: ['house']}, vernWs, analysisWs); + expect(result).toEqual([]); + }); +}); diff --git a/frontend/viewer/src/lib/entry-editor/duplicate-check.ts b/frontend/viewer/src/lib/entry-editor/duplicate-check.ts new file mode 100644 index 0000000000..f4a324b781 --- /dev/null +++ b/frontend/viewer/src/lib/entry-editor/duplicate-check.ts @@ -0,0 +1,238 @@ +import type {IEntry, IMorphType, ISense} from '$lib/dotnet-types'; + +export const MIN_QUERY_LENGTH = 2; + +/** + * Ordered strongest to weakest. Candidates the backend's full-text search returned but that + * don't match one of these — a typed vernacular value that only hit a gloss, or a typed gloss + * that only hit a headword — are dropped rather than shown as a vague "related" hit: for a + * duplicate warning those cross-field coincidences are noise, not signal. + */ +export type DuplicateMatchKind = 'same-word' | 'similar-word' | 'same-meaning'; + +/** + * Which field an exact match hit: 'headword' when the citation form matched (or the lexeme + * form of an entry with no citation form — the lexeme IS the headword then), 'lexeme' when + * only the lexeme form matched while a different citation form exists. Same severity either + * way; it only exists so the badge doesn't claim "Same headword" for a lexeme-only match. + */ +export type SameWordField = 'headword' | 'lexeme'; + +export interface DuplicateMatch { + entry: IEntry; + kind: DuplicateMatchKind; + field?: SameWordField; +} + +export interface VernacularQuery { + text: string; + /** The writing system the text was typed in — used to rank that WS's headword matches first */ + wsId: string; +} + +export interface DuplicateQueries { + /** Texts the user typed into vernacular fields (lexeme form, citation form) */ + vernacular: VernacularQuery[]; + /** Texts the user typed into gloss fields */ + analysis: string[]; +} + +export type MorphTokenSource = Pick; + +export function duplicateTintClass(hasExactWordMatch: boolean): string { + return hasExactWordMatch + ? 'border-amber-600/40 bg-amber-500/10 dark:border-amber-400/40' + : 'border-border bg-muted/50'; +} + +/** Hosts submit on Enter; interacting with duplicate UI must never also create the entry. */ +export function trapEnter(event: KeyboardEvent): void { + if (event.key === 'Enter') event.stopPropagation(); +} + +export const MAX_SIMILAR_LENGTH_DELTA = 5; + +/** + * Deliberately broader than a starts-with headword match: mid-word containment must count + * (typing "liman" has to surface an existing "naliman"), so it's containment in either + * direction with no positional threshold. The length-delta cap keeps short fragments from + * matching half the lexicon (typing "uz" must not surface every word containing it). + */ +export function isSimilarWord(a: string, b: string): boolean { + const [shorter, longer] = a.length <= b.length ? [a, b] : [b, a]; + return longer.length - shorter.length <= MAX_SIMILAR_LENGTH_DELTA && longer.includes(shorter); +} + +const kindRank: Record = { + 'same-word': 0, + 'similar-word': 1, + 'same-meaning': 2, +}; + +/** + * Mirrors the backend fold (SqlHelpers.ContainsIgnoreCaseAccents → StringExtensions): + * invariant case fold, and diacritics are significant only when the search text itself + * contains them — hence the keepDiacritics mode, chosen per query via hasDiacritics(). + * Collation-level equivalences (ß≈ss, ligatures, ICU-ignorable characters like soft hyphens) + * are not replicated. + */ +export function normalizeForCompare(value: string, keepDiacritics = false): string { + const decomposed = value.normalize('NFD'); + const folded = keepDiacritics ? decomposed : decomposed.replace(/\p{Mn}/gu, ''); + return folded.toLowerCase().trim(); +} + +export function hasDiacritics(value: string): boolean { + return /\p{Mn}/u.test(value.normalize('NFD')); +} + +/** Mirrors the backend's EntrySearchService.StripMorphTokens best-match strip. */ +export function stripMorphTokens(value: string, morphTypes: readonly MorphTokenSource[]): string { + let bestScore = 0; + let bestMatch: MorphTokenSource | undefined; + for (const morphType of morphTypes) { + const prefix = morphType.prefix?.trim() ? morphType.prefix : undefined; + const postfix = morphType.postfix?.trim() ? morphType.postfix : undefined; + const matchesPrefix = !!prefix && value.startsWith(prefix); + const matchesPostfix = !!postfix && value.endsWith(postfix) + && (!matchesPrefix || value.length >= prefix.length + postfix.length); + const score = (matchesPrefix ? 2 : 0) + (matchesPostfix ? 1 : 0); + if (score > bestScore) { + bestScore = score; + bestMatch = morphType; + } + } + if (!bestMatch) return value; + let result = value; + if (bestMatch.prefix?.trim() && result.startsWith(bestMatch.prefix)) result = result.slice(bestMatch.prefix.length); + if (bestMatch.postfix?.trim() && result.endsWith(bestMatch.postfix)) result = result.slice(0, -bestMatch.postfix.length); + return result; +} + +export function duplicateQueries( + entry: Pick, + sense: Pick | undefined, + vernacularWsIds: string[], + analysisWsIds: string[], +): DuplicateQueries { + // dedupe per field kind, not across: the same text typed as both lexeme and gloss must + // still produce an analysis query, or its same-meaning matches are never classified + const seenVernacular = new Set(); + const vernacular: VernacularQuery[] = []; + for (const wsId of vernacularWsIds) { + for (const value of [entry.lexemeForm?.[wsId], entry.citationForm?.[wsId]]) { + const trimmed = value?.trim(); + if (!trimmed) continue; + const normalized = normalizeForCompare(trimmed); + if (normalized.length < MIN_QUERY_LENGTH || seenVernacular.has(normalized)) continue; + seenVernacular.add(normalized); + vernacular.push({text: trimmed, wsId}); + } + } + + const seenAnalysis = new Set(); + const analysis: string[] = []; + for (const value of analysisWsIds.map(wsId => sense?.gloss?.[wsId])) { + const trimmed = value?.trim(); + if (!trimmed) continue; + const normalized = normalizeForCompare(trimmed); + if (normalized.length < MIN_QUERY_LENGTH || seenAnalysis.has(normalized)) continue; + seenAnalysis.add(normalized); + analysis.push(trimmed); + } + return {vernacular, analysis}; +} + +/** Merges per-query search results into a single relevance-ordered candidate list. */ +export function mergeSearchResults(results: IEntry[][]): IEntry[] { + const seen = new Set(); + return results.flat().filter(entry => { + if (seen.has(entry.id)) return false; + seen.add(entry.id); + return true; + }); +} + +/** + * Classifies search results against what the user typed, drops cross-field coincidences, and + * orders survivors strongest match first (headword matches before gloss matches; similar words + * closest in length first). `candidates` are expected in search-relevance order, which is + * preserved between equally-strong matches. + * + * Match rules mirror the CRDT FTS backend (EntrySearchService): a typed value counts against a + * lexeme or citation form in any vernacular WS, but typed morph tokens (e.g. the "-" on a suffix) + * are stripped before comparing against lexeme forms and kept when comparing against citation + * forms. The FwData search and the backend's short-query (< 3 chars) fallback never strip morph + * tokens — that only affects which candidates arrive, not how they're classified here. + */ +export function classifyDuplicates( + candidates: IEntry[], + queries: DuplicateQueries, + vernacularWsIds: string[], + analysisWsIds: string[], + morphTypes: readonly MorphTokenSource[] = [], +): DuplicateMatch[] { + const vernQueries = queries.vernacular.map(({text}) => { + const keepDiacritics = hasDiacritics(text); + return { + keepDiacritics, + lexeme: normalizeForCompare(stripMorphTokens(text, morphTypes), keepDiacritics), + citation: normalizeForCompare(text, keepDiacritics), + }; + }); + const analysisQueries = queries.analysis.map(text => ({ + keepDiacritics: hasDiacritics(text), + text, + })); + + function classify(entry: IEntry): {kind: DuplicateMatchKind, field?: SameWordField, lengthDelta: number} | undefined { + const lexemeForms = vernacularWsIds.map(wsId => entry.lexemeForm?.[wsId]).filter((form): form is string => !!form); + const citationForms = vernacularWsIds.map(wsId => entry.citationForm?.[wsId]).filter((form): form is string => !!form); + if (lexemeForms.length || citationForms.length) { + let sameWordField: SameWordField | undefined; + for (const query of vernQueries) { + const lex = lexemeForms.map(form => normalizeForCompare(form, query.keepDiacritics)); + const cit = citationForms.map(form => normalizeForCompare(form, query.keepDiacritics)); + if (cit.includes(query.citation)) { + sameWordField = 'headword'; + break; + } + if (lex.includes(query.lexeme)) sameWordField ??= citationForms.length ? 'lexeme' : 'headword'; + } + if (sameWordField) return {kind: 'same-word', field: sameWordField, lengthDelta: 0}; + let lengthDelta = Infinity; + for (const query of vernQueries) { + const lex = lexemeForms.map(form => normalizeForCompare(form, query.keepDiacritics)); + const cit = citationForms.map(form => normalizeForCompare(form, query.keepDiacritics)); + for (const {form, queryText} of [ + ...lex.map(form => ({form, queryText: query.lexeme})), + ...cit.map(form => ({form, queryText: query.citation})), + ]) { + if (isSimilarWord(form, queryText)) { + lengthDelta = Math.min(lengthDelta, Math.abs(form.length - queryText.length)); + } + } + } + if (lengthDelta !== Infinity) return {kind: 'similar-word', lengthDelta}; + } + if (analysisQueries.length) { + const rawGlosses = (entry.senses ?? []) + .flatMap(sense => analysisWsIds.map(wsId => sense.gloss?.[wsId])) + .filter((gloss): gloss is string => !!gloss); + for (const query of analysisQueries) { + const queryText = normalizeForCompare(query.text, query.keepDiacritics); + const glosses = rawGlosses.map(gloss => normalizeForCompare(gloss, query.keepDiacritics)); + if (glosses.some(gloss => gloss.includes(queryText) || queryText.includes(gloss))) { + return {kind: 'same-meaning', lengthDelta: 0}; + } + } + } + return undefined; + } + + return candidates + .map(entry => ({entry, match: classify(entry)})) + .filter((candidate): candidate is {entry: IEntry, match: NonNullable>} => candidate.match !== undefined) + .sort((a, b) => (kindRank[a.match.kind] - kindRank[b.match.kind]) || (a.match.lengthDelta - b.match.lengthDelta)) + .map(({entry, match}) => ({entry, kind: match.kind, field: match.field})); +} diff --git a/frontend/viewer/src/locales/en.po b/frontend/viewer/src/locales/en.po index c4b684090a..6388b6bc91 100644 --- a/frontend/viewer/src/locales/en.po +++ b/frontend/viewer/src/locales/en.po @@ -20,6 +20,7 @@ msgstr "" msgid "– ({0} changes)" msgstr "– ({0} changes)" +#: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte msgid "(unknown)" msgstr "(unknown)" @@ -101,6 +102,20 @@ msgstr "**Simpler terminology** (e.g. *Word* instead of *Lexeme form*, *Meaning* msgid "A new version of FieldWorks Lite is available." msgstr "A new version of FieldWorks Lite is available." +#. Relevant view: Classic +#. Lite view equivalent: "A similar word already exists" +#. Warning banner in the New Entry dialog when exactly one possible duplicate was found (none an exact headword match) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar entry already exists" +msgstr "A similar entry already exists" + +#. Relevant view: Lite +#. Classic view equivalent: "A similar entry already exists" +#. Warning banner in the New Word dialog when exactly one possible duplicate was found (none spelled exactly the same) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar word already exists" +msgstr "A similar word already exists" + #. Relevant view: Lite #. Classic view equivalent: "an entry" #: src/project/browse/BrowseView.svelte @@ -151,6 +166,13 @@ msgstr "Add component" msgid "Add Example" msgstr "Add Example" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense" +#. Short button label on a possible-duplicate result in the New Word dialog; fuller form: "Add meaning to this word" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning" +msgstr "Add meaning" + #. Button label #. Relevant view: Lite #. Classic view equivalent: "Add Sense" @@ -160,6 +182,13 @@ msgstr "Add Example" msgid "Add Meaning" msgstr "Add Meaning" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense to this entry" +#. Button on a possible-duplicate result in the New Word dialog: saves the meaning being typed onto that existing word (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning to this word" +msgstr "Add meaning to this word" + #. Button in toolbar #: src/lib/entry-editor/NewEntryButton.svelte msgid "Add new word" @@ -172,6 +201,13 @@ msgstr "Add new word" msgid "Add part of" msgstr "Add part of" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning" +#. Short button label on a possible-duplicate result in the New Entry dialog; fuller form: "Add sense to this entry" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense" +msgstr "Add sense" + #. Relevant view: Classic #. Lite view equivalent: "Add Meaning" #. Small button to add another sense/meaning for a word @@ -180,6 +216,13 @@ msgstr "Add part of" msgid "Add Sense" msgstr "Add Sense" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning to this word" +#. Button on a possible-duplicate result in the New Entry dialog: saves the sense being typed onto that existing entry (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense to this entry" +msgstr "Add sense to this entry" + #. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state) #. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text #: src/project/browse/EntriesList.svelte @@ -369,6 +412,20 @@ msgstr "Changes can also be the result of fields being added to new versions of msgid "Changing the same field twice, for example, can result in two commits but only one change that needs to be applied to FieldWorks Classic. Commits can also consist of multiple changes." msgstr "Changing the same field twice, for example, can result in two commits but only one change that needs to be applied to FieldWorks Classic. Commits can also consist of multiple changes." +#. Relevant view: Classic +#. Lite view equivalent: "Checking for similar words…" +#. Status line in the New Entry dialog while searching for possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar entries…" +msgstr "Checking for similar entries…" + +#. Relevant view: Lite +#. Classic view equivalent: "Checking for similar entries…" +#. Status line in the New Word dialog while searching for possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar words…" +msgstr "Checking for similar words…" + #. Status message while checking for updates #: src/lib/updates/UpdateDialogContent.svelte msgid "Checking for updates..." @@ -405,6 +462,7 @@ msgstr "clear" #: src/lib/components/ui/button/x-button.svelte #: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte +#: src/lib/entry-editor/DuplicateSummaryPill.svelte #: src/project/tasks/SubjectPopup.svelte msgid "Close" msgstr "Close" @@ -1071,6 +1129,20 @@ msgstr "Gloss" msgid "Go to {0}" msgstr "Go to {0}" +#. Relevant view: Classic +#. Lite view equivalent: "Go to word" +#. Tooltip on a possible-duplicate result in the New Entry dialog; clicking closes the dialog and opens that entry +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to entry" +msgstr "Go to entry" + +#. Relevant view: Lite +#. Classic view equivalent: "Go to entry" +#. Tooltip on a possible-duplicate result in the New Word dialog; clicking closes the dialog and opens that word +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to word" +msgstr "Go to word" + #. Filter option label #. Relevant view: Classic #. Lite view equivalent: "Part of speech" @@ -1263,6 +1335,13 @@ msgstr "Login to see projects" msgid "Logout" msgstr "Logout" +#. Relevant view: Lite +#. Classic view equivalent: "No similar entries found" +#. Reassuring status line in the New Word dialog: the duplicate check found no existing words like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Looks like a new word" +msgstr "Looks like a new word" + #. Footer text in the project sidebar. The emoji flags are the countries of origin of the development team — do not translate. #: src/project/ProjectSidebar.svelte msgid "Made with ❤️ from 🇦🇹 🇹🇭 🇺🇸" @@ -1308,6 +1387,13 @@ msgstr "Manual update is required. Please follow the instructions provided." msgid "Meaning" msgstr "Meaning" +#. Relevant view: Lite +#. Classic view equivalent: "Sense added to \"{0}\"" +#. Success notification after adding the typed meaning to an existing word; {0} is that word's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Meaning added to \"{0}\"" +msgstr "Meaning added to \"{0}\"" + #. Task subject label in the task list. {0} = writing system abbreviation (e.g., "en", "fr"). Identifies entries lacking a definition in that writing system. #: src/project/tasks/tasks-service.ts msgid "Missing Definition {0}" @@ -1492,6 +1578,13 @@ msgstr "No server" msgid "No server configured" msgstr "No server configured" +#. Relevant view: Classic +#. Lite view equivalent: "Looks like a new word" +#. Status line in the New Entry dialog: the duplicate check found no existing entries like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "No similar entries found" +msgstr "No similar entries found" + #. Error message in the task editing drawer when no subject entity can be found. {0} = subject type (e.g., "sense", "example-sentence"). #: src/project/tasks/SubjectPopup.svelte msgid "No subject, unable to create a new {0}" @@ -1782,6 +1875,28 @@ msgstr "Review" msgid "rose" msgstr "rose" +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: its headword is identical to the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same headword" +msgstr "Same headword" + +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: only its lexeme form matches the typed text, while its citation form (shown as the headword) differs — deliberately NOT claiming "Same headword" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same lexeme form" +msgstr "Same lexeme form" + +#. Relevant view: Lite +#. Classic view equivalent: "Same headword" +#. Badge on a possible-duplicate result: it is spelled exactly like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same word" +msgstr "Same word" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Save" msgstr "Save" @@ -1872,6 +1987,13 @@ msgstr "Send us a message" msgid "Sense" msgstr "Sense" +#. Relevant view: Classic +#. Lite view equivalent: "Meaning added to \"{0}\"" +#. Success notification after adding the typed sense to an existing entry; {0} is that entry's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Sense added to \"{0}\"" +msgstr "Sense added to \"{0}\"" + #. Field label for example text #. The sentence/phrase text for an example (source text in original language) #. Paired with "Translation" field for translated version @@ -1910,6 +2032,7 @@ msgid "Show" msgstr "Show" #. Expand button with count +#: src/lib/entry-editor/DuplicateCheck.svelte #: src/lib/entry-editor/EntryOrSensePicker.svelte msgid "Show {0} more..." msgstr "Show {0} more..." @@ -1918,6 +2041,48 @@ msgstr "Show {0} more..." msgid "Sign in to add comments." msgstr "Sign in to add comments." +#. Relevant view: Classic +#. Lite view equivalent: "Similar words already exist" +#. Warning banner in the New Entry dialog; expands to a list of possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar entries already exist" +msgstr "Similar entries already exist" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar meaning" +#. Badge on a possible-duplicate result: one of its glosses matches the gloss being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar gloss" +msgstr "Similar gloss" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar word" +#. Badge on a possible-duplicate result: its headword partly matches the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar headword" +msgstr "Similar headword" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar gloss" +#. Badge on a possible-duplicate result: one of its meanings matches the meaning being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar meaning" +msgstr "Similar meaning" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar headword" +#. Badge on a possible-duplicate result: it is spelled almost like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar word" +msgstr "Similar word" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar entries already exist" +#. Warning banner in the New Word dialog; expands to a list of possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar words already exist" +msgstr "Similar words already exist" + #. View option #: src/project/browse/EntryListViewOptions.svelte msgid "Simple" @@ -2115,6 +2280,13 @@ msgstr "This {0} was deleted" msgid "This date # and this emoji # are snippets" msgstr "This date # and this emoji # are snippets" +#. Relevant view: Classic +#. Lite view equivalent: "This word may already exist" +#. Warning banner in the New Entry dialog when an entry with the exact same headword exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This entry may already exist" +msgstr "This entry may already exist" + #. Help message #: src/lib/components/OpenInFieldWorksButton.svelte msgid "This project is now open in FieldWorks. To continue working in FieldWorks Lite, close the project in FieldWorks and click Reopen." @@ -2130,6 +2302,13 @@ msgstr "This task is complete" msgid "This will synchronize the FieldWorks Lite and FieldWorks Classic copies of your project in Lexbox." msgstr "This will synchronize the FieldWorks Lite and FieldWorks Classic copies of your project in Lexbox." +#. Relevant view: Lite +#. Classic view equivalent: "This entry may already exist" +#. Warning banner in the New Word dialog when a word with the exact same spelling exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This word may already exist" +msgstr "This word may already exist" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Thread" msgstr "Thread" diff --git a/frontend/viewer/src/locales/es.po b/frontend/viewer/src/locales/es.po index 9f5a491ec1..e82f77274b 100644 --- a/frontend/viewer/src/locales/es.po +++ b/frontend/viewer/src/locales/es.po @@ -25,6 +25,7 @@ msgstr "" msgid "– ({0} changes)" msgstr "– ({0} cambios)" +#: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte msgid "(unknown)" msgstr "" @@ -106,6 +107,20 @@ msgstr "**Terminología más simple** (p. ej. *Palabra* en lugar de *Forma del l msgid "A new version of FieldWorks Lite is available." msgstr "Una nueva versión de FieldWorks Lite está disponible." +#. Relevant view: Classic +#. Lite view equivalent: "A similar word already exists" +#. Warning banner in the New Entry dialog when exactly one possible duplicate was found (none an exact headword match) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar entry already exists" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "A similar entry already exists" +#. Warning banner in the New Word dialog when exactly one possible duplicate was found (none spelled exactly the same) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar word already exists" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "an entry" #: src/project/browse/BrowseView.svelte @@ -156,6 +171,13 @@ msgstr "Añadir componente" msgid "Add Example" msgstr "Añadir ejemplo" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense" +#. Short button label on a possible-duplicate result in the New Word dialog; fuller form: "Add meaning to this word" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning" +msgstr "" + #. Button label #. Relevant view: Lite #. Classic view equivalent: "Add Sense" @@ -165,6 +187,13 @@ msgstr "Añadir ejemplo" msgid "Add Meaning" msgstr "Añadir significado" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense to this entry" +#. Button on a possible-duplicate result in the New Word dialog: saves the meaning being typed onto that existing word (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning to this word" +msgstr "" + #. Button in toolbar #: src/lib/entry-editor/NewEntryButton.svelte msgid "Add new word" @@ -177,6 +206,13 @@ msgstr "Añadir nueva palabra" msgid "Add part of" msgstr "Añadir parte de" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning" +#. Short button label on a possible-duplicate result in the New Entry dialog; fuller form: "Add sense to this entry" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense" +msgstr "" + #. Relevant view: Classic #. Lite view equivalent: "Add Meaning" #. Small button to add another sense/meaning for a word @@ -185,6 +221,13 @@ msgstr "Añadir parte de" msgid "Add Sense" msgstr "Añadir acepción" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning to this word" +#. Button on a possible-duplicate result in the New Entry dialog: saves the sense being typed onto that existing entry (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense to this entry" +msgstr "" + #. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state) #. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text #: src/project/browse/EntriesList.svelte @@ -374,6 +417,20 @@ msgstr "Los cambios también pueden ser el resultado de que los campos sean agre msgid "Changing the same field twice, for example, can result in two commits but only one change that needs to be applied to FieldWorks Classic. Commits can also consist of multiple changes." msgstr "Cambiar el mismo campo dos veces, por ejemplo, puede resultar en dos commits pero sólo un cambio que necesita ser aplicado a FieldWorks Classic. Los commits también pueden consistir en varios cambios." +#. Relevant view: Classic +#. Lite view equivalent: "Checking for similar words…" +#. Status line in the New Entry dialog while searching for possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar entries…" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Checking for similar entries…" +#. Status line in the New Word dialog while searching for possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar words…" +msgstr "" + #. Status message while checking for updates #: src/lib/updates/UpdateDialogContent.svelte msgid "Checking for updates..." @@ -410,6 +467,7 @@ msgstr "borrar" #: src/lib/components/ui/button/x-button.svelte #: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte +#: src/lib/entry-editor/DuplicateSummaryPill.svelte #: src/project/tasks/SubjectPopup.svelte msgid "Close" msgstr "Cerrar" @@ -1076,6 +1134,20 @@ msgstr "Glosa" msgid "Go to {0}" msgstr "Visite {0}" +#. Relevant view: Classic +#. Lite view equivalent: "Go to word" +#. Tooltip on a possible-duplicate result in the New Entry dialog; clicking closes the dialog and opens that entry +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to entry" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Go to entry" +#. Tooltip on a possible-duplicate result in the New Word dialog; clicking closes the dialog and opens that word +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to word" +msgstr "" + #. Filter option label #. Relevant view: Classic #. Lite view equivalent: "Part of speech" @@ -1268,6 +1340,13 @@ msgstr "Inicie sesión para ver los proyectos" msgid "Logout" msgstr "Cierre de sesión" +#. Relevant view: Lite +#. Classic view equivalent: "No similar entries found" +#. Reassuring status line in the New Word dialog: the duplicate check found no existing words like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Looks like a new word" +msgstr "" + #. Footer text in the project sidebar. The emoji flags are the countries of origin of the development team — do not translate. #: src/project/ProjectSidebar.svelte msgid "Made with ❤️ from 🇦🇹 🇹🇭 🇺🇸" @@ -1313,6 +1392,13 @@ msgstr "Se requiere una actualización manual. Siga las instrucciones proporcion msgid "Meaning" msgstr "Significado" +#. Relevant view: Lite +#. Classic view equivalent: "Sense added to \"{0}\"" +#. Success notification after adding the typed meaning to an existing word; {0} is that word's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Meaning added to \"{0}\"" +msgstr "" + #. Task subject label in the task list. {0} = writing system abbreviation (e.g., "en", "fr"). Identifies entries lacking a definition in that writing system. #: src/project/tasks/tasks-service.ts msgid "Missing Definition {0}" @@ -1497,6 +1583,13 @@ msgstr "Sin servidor" msgid "No server configured" msgstr "Ningún servidor configurado" +#. Relevant view: Classic +#. Lite view equivalent: "Looks like a new word" +#. Status line in the New Entry dialog: the duplicate check found no existing entries like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "No similar entries found" +msgstr "" + #. Error message in the task editing drawer when no subject entity can be found. {0} = subject type (e.g., "sense", "example-sentence"). #: src/project/tasks/SubjectPopup.svelte msgid "No subject, unable to create a new {0}" @@ -1787,6 +1880,28 @@ msgstr "Revisión" msgid "rose" msgstr "rosa" +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: its headword is identical to the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same headword" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: only its lexeme form matches the typed text, while its citation form (shown as the headword) differs — deliberately NOT claiming "Same headword" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same lexeme form" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Same headword" +#. Badge on a possible-duplicate result: it is spelled exactly like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same word" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Save" msgstr "" @@ -1877,6 +1992,13 @@ msgstr "Envíenos un mensaje" msgid "Sense" msgstr "Acepción" +#. Relevant view: Classic +#. Lite view equivalent: "Meaning added to \"{0}\"" +#. Success notification after adding the typed sense to an existing entry; {0} is that entry's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Sense added to \"{0}\"" +msgstr "" + #. Field label for example text #. The sentence/phrase text for an example (source text in original language) #. Paired with "Translation" field for translated version @@ -1915,6 +2037,7 @@ msgid "Show" msgstr "Mostrar" #. Expand button with count +#: src/lib/entry-editor/DuplicateCheck.svelte #: src/lib/entry-editor/EntryOrSensePicker.svelte msgid "Show {0} more..." msgstr "Mostrar {0} más..." @@ -1923,6 +2046,48 @@ msgstr "Mostrar {0} más..." msgid "Sign in to add comments." msgstr "" +#. Relevant view: Classic +#. Lite view equivalent: "Similar words already exist" +#. Warning banner in the New Entry dialog; expands to a list of possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar entries already exist" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar meaning" +#. Badge on a possible-duplicate result: one of its glosses matches the gloss being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar gloss" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar word" +#. Badge on a possible-duplicate result: its headword partly matches the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar headword" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar gloss" +#. Badge on a possible-duplicate result: one of its meanings matches the meaning being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar meaning" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar headword" +#. Badge on a possible-duplicate result: it is spelled almost like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar word" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar entries already exist" +#. Warning banner in the New Word dialog; expands to a list of possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar words already exist" +msgstr "" + #. View option #: src/project/browse/EntryListViewOptions.svelte msgid "Simple" @@ -2120,6 +2285,13 @@ msgstr "Este {0} ha sido eliminado" msgid "This date # and this emoji # are snippets" msgstr "Esta fecha # y este emoji # son fragmentos" +#. Relevant view: Classic +#. Lite view equivalent: "This word may already exist" +#. Warning banner in the New Entry dialog when an entry with the exact same headword exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This entry may already exist" +msgstr "" + #. Help message #: src/lib/components/OpenInFieldWorksButton.svelte msgid "This project is now open in FieldWorks. To continue working in FieldWorks Lite, close the project in FieldWorks and click Reopen." @@ -2135,6 +2307,13 @@ msgstr "Esta tarea está completa" msgid "This will synchronize the FieldWorks Lite and FieldWorks Classic copies of your project in Lexbox." msgstr "Esto sincronizará las copias de FieldWorks Lite y FieldWorks Classic de su proyecto en Lexbox." +#. Relevant view: Lite +#. Classic view equivalent: "This entry may already exist" +#. Warning banner in the New Word dialog when a word with the exact same spelling exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This word may already exist" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Thread" msgstr "" diff --git a/frontend/viewer/src/locales/fr.po b/frontend/viewer/src/locales/fr.po index be746b3ab0..ab93463ebb 100644 --- a/frontend/viewer/src/locales/fr.po +++ b/frontend/viewer/src/locales/fr.po @@ -25,6 +25,7 @@ msgstr "" msgid "– ({0} changes)" msgstr "– ({0} changements)" +#: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte msgid "(unknown)" msgstr "" @@ -106,6 +107,20 @@ msgstr "**terminologie plus simple** (par exemple *Mot* au lieu de *forme de lex msgid "A new version of FieldWorks Lite is available." msgstr "Une nouvelle version de FieldWorks Lite est disponible." +#. Relevant view: Classic +#. Lite view equivalent: "A similar word already exists" +#. Warning banner in the New Entry dialog when exactly one possible duplicate was found (none an exact headword match) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar entry already exists" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "A similar entry already exists" +#. Warning banner in the New Word dialog when exactly one possible duplicate was found (none spelled exactly the same) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar word already exists" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "an entry" #: src/project/browse/BrowseView.svelte @@ -156,6 +171,13 @@ msgstr "Ajouter un composant" msgid "Add Example" msgstr "Ajouter un exemple" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense" +#. Short button label on a possible-duplicate result in the New Word dialog; fuller form: "Add meaning to this word" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning" +msgstr "" + #. Button label #. Relevant view: Lite #. Classic view equivalent: "Add Sense" @@ -165,6 +187,13 @@ msgstr "Ajouter un exemple" msgid "Add Meaning" msgstr "Ajouter une signification" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense to this entry" +#. Button on a possible-duplicate result in the New Word dialog: saves the meaning being typed onto that existing word (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning to this word" +msgstr "" + #. Button in toolbar #: src/lib/entry-editor/NewEntryButton.svelte msgid "Add new word" @@ -177,6 +206,13 @@ msgstr "Ajouter un nouveau mot" msgid "Add part of" msgstr "Ajouter une partie de" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning" +#. Short button label on a possible-duplicate result in the New Entry dialog; fuller form: "Add sense to this entry" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense" +msgstr "" + #. Relevant view: Classic #. Lite view equivalent: "Add Meaning" #. Small button to add another sense/meaning for a word @@ -185,6 +221,13 @@ msgstr "Ajouter une partie de" msgid "Add Sense" msgstr "Ajouter du sens" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning to this word" +#. Button on a possible-duplicate result in the New Entry dialog: saves the sense being typed onto that existing entry (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense to this entry" +msgstr "" + #. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state) #. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text #: src/project/browse/EntriesList.svelte @@ -374,6 +417,20 @@ msgstr "Les modifications peuvent également résulter de l'ajout de champs à d msgid "Changing the same field twice, for example, can result in two commits but only one change that needs to be applied to FieldWorks Classic. Commits can also consist of multiple changes." msgstr "Changer le même champ deux fois, par exemple, peut entraîner deux livraisons mais un seul changement qui doit être appliqué à FieldWorks Classic. Les commits peuvent également comporter de multiples changements." +#. Relevant view: Classic +#. Lite view equivalent: "Checking for similar words…" +#. Status line in the New Entry dialog while searching for possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar entries…" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Checking for similar entries…" +#. Status line in the New Word dialog while searching for possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar words…" +msgstr "" + #. Status message while checking for updates #: src/lib/updates/UpdateDialogContent.svelte msgid "Checking for updates..." @@ -410,6 +467,7 @@ msgstr "effacer" #: src/lib/components/ui/button/x-button.svelte #: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte +#: src/lib/entry-editor/DuplicateSummaryPill.svelte #: src/project/tasks/SubjectPopup.svelte msgid "Close" msgstr "Fermer" @@ -1076,6 +1134,20 @@ msgstr "Glose" msgid "Go to {0}" msgstr "Naviguer à {0}" +#. Relevant view: Classic +#. Lite view equivalent: "Go to word" +#. Tooltip on a possible-duplicate result in the New Entry dialog; clicking closes the dialog and opens that entry +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to entry" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Go to entry" +#. Tooltip on a possible-duplicate result in the New Word dialog; clicking closes the dialog and opens that word +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to word" +msgstr "" + #. Filter option label #. Relevant view: Classic #. Lite view equivalent: "Part of speech" @@ -1268,6 +1340,13 @@ msgstr "Se connecter pour voir les projets" msgid "Logout" msgstr "Déconnexion" +#. Relevant view: Lite +#. Classic view equivalent: "No similar entries found" +#. Reassuring status line in the New Word dialog: the duplicate check found no existing words like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Looks like a new word" +msgstr "" + #. Footer text in the project sidebar. The emoji flags are the countries of origin of the development team — do not translate. #: src/project/ProjectSidebar.svelte msgid "Made with ❤️ from 🇦🇹 🇹🇭 🇺🇸" @@ -1313,6 +1392,13 @@ msgstr "Une mise à jour manuelle est requise. Veuillez suivre les instructions msgid "Meaning" msgstr "Signification" +#. Relevant view: Lite +#. Classic view equivalent: "Sense added to \"{0}\"" +#. Success notification after adding the typed meaning to an existing word; {0} is that word's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Meaning added to \"{0}\"" +msgstr "" + #. Task subject label in the task list. {0} = writing system abbreviation (e.g., "en", "fr"). Identifies entries lacking a definition in that writing system. #: src/project/tasks/tasks-service.ts msgid "Missing Definition {0}" @@ -1497,6 +1583,13 @@ msgstr "Aucun serveur" msgid "No server configured" msgstr "Pas de serveur configuré" +#. Relevant view: Classic +#. Lite view equivalent: "Looks like a new word" +#. Status line in the New Entry dialog: the duplicate check found no existing entries like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "No similar entries found" +msgstr "" + #. Error message in the task editing drawer when no subject entity can be found. {0} = subject type (e.g., "sense", "example-sentence"). #: src/project/tasks/SubjectPopup.svelte msgid "No subject, unable to create a new {0}" @@ -1787,6 +1880,28 @@ msgstr "Évaluer" msgid "rose" msgstr "rose" +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: its headword is identical to the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same headword" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: only its lexeme form matches the typed text, while its citation form (shown as the headword) differs — deliberately NOT claiming "Same headword" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same lexeme form" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Same headword" +#. Badge on a possible-duplicate result: it is spelled exactly like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same word" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Save" msgstr "" @@ -1877,6 +1992,13 @@ msgstr "Envoyez-nous un message" msgid "Sense" msgstr "Sens" +#. Relevant view: Classic +#. Lite view equivalent: "Meaning added to \"{0}\"" +#. Success notification after adding the typed sense to an existing entry; {0} is that entry's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Sense added to \"{0}\"" +msgstr "" + #. Field label for example text #. The sentence/phrase text for an example (source text in original language) #. Paired with "Translation" field for translated version @@ -1915,6 +2037,7 @@ msgid "Show" msgstr "Afficher" #. Expand button with count +#: src/lib/entry-editor/DuplicateCheck.svelte #: src/lib/entry-editor/EntryOrSensePicker.svelte msgid "Show {0} more..." msgstr "Afficher {0} plus..." @@ -1923,6 +2046,48 @@ msgstr "Afficher {0} plus..." msgid "Sign in to add comments." msgstr "" +#. Relevant view: Classic +#. Lite view equivalent: "Similar words already exist" +#. Warning banner in the New Entry dialog; expands to a list of possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar entries already exist" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar meaning" +#. Badge on a possible-duplicate result: one of its glosses matches the gloss being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar gloss" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar word" +#. Badge on a possible-duplicate result: its headword partly matches the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar headword" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar gloss" +#. Badge on a possible-duplicate result: one of its meanings matches the meaning being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar meaning" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar headword" +#. Badge on a possible-duplicate result: it is spelled almost like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar word" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar entries already exist" +#. Warning banner in the New Word dialog; expands to a list of possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar words already exist" +msgstr "" + #. View option #: src/project/browse/EntryListViewOptions.svelte msgid "Simple" @@ -2120,6 +2285,13 @@ msgstr "Cet {0} a été supprimé" msgid "This date # and this emoji # are snippets" msgstr "Cette date # et cet emoji # sont des extraits." +#. Relevant view: Classic +#. Lite view equivalent: "This word may already exist" +#. Warning banner in the New Entry dialog when an entry with the exact same headword exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This entry may already exist" +msgstr "" + #. Help message #: src/lib/components/OpenInFieldWorksButton.svelte msgid "This project is now open in FieldWorks. To continue working in FieldWorks Lite, close the project in FieldWorks and click Reopen." @@ -2135,6 +2307,13 @@ msgstr "Cette tâche est terminée" msgid "This will synchronize the FieldWorks Lite and FieldWorks Classic copies of your project in Lexbox." msgstr "Cela synchronisera les copies FieldWorks Lite et FieldWorks Classic de votre projet dans Lexbox." +#. Relevant view: Lite +#. Classic view equivalent: "This entry may already exist" +#. Warning banner in the New Word dialog when a word with the exact same spelling exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This word may already exist" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Thread" msgstr "" diff --git a/frontend/viewer/src/locales/id.po b/frontend/viewer/src/locales/id.po index 13cbd2629f..105de2221e 100644 --- a/frontend/viewer/src/locales/id.po +++ b/frontend/viewer/src/locales/id.po @@ -25,6 +25,7 @@ msgstr "" msgid "– ({0} changes)" msgstr "- ({0} perubahan)" +#: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte msgid "(unknown)" msgstr "" @@ -106,6 +107,20 @@ msgstr "**Terminologi yang lebih sederhana** (mis. *Kata* alih-alih *Bentuk leks msgid "A new version of FieldWorks Lite is available." msgstr "Versi baru FieldWorks Lite telah tersedia." +#. Relevant view: Classic +#. Lite view equivalent: "A similar word already exists" +#. Warning banner in the New Entry dialog when exactly one possible duplicate was found (none an exact headword match) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar entry already exists" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "A similar entry already exists" +#. Warning banner in the New Word dialog when exactly one possible duplicate was found (none spelled exactly the same) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar word already exists" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "an entry" #: src/project/browse/BrowseView.svelte @@ -156,6 +171,13 @@ msgstr "Menambahkan komponen" msgid "Add Example" msgstr "Tambahkan Contoh" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense" +#. Short button label on a possible-duplicate result in the New Word dialog; fuller form: "Add meaning to this word" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning" +msgstr "" + #. Button label #. Relevant view: Lite #. Classic view equivalent: "Add Sense" @@ -165,6 +187,13 @@ msgstr "Tambahkan Contoh" msgid "Add Meaning" msgstr "Tambahkan Makna" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense to this entry" +#. Button on a possible-duplicate result in the New Word dialog: saves the meaning being typed onto that existing word (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning to this word" +msgstr "" + #. Button in toolbar #: src/lib/entry-editor/NewEntryButton.svelte msgid "Add new word" @@ -177,6 +206,13 @@ msgstr "Tambahkan kata baru" msgid "Add part of" msgstr "Tambahkan bagian dari" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning" +#. Short button label on a possible-duplicate result in the New Entry dialog; fuller form: "Add sense to this entry" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense" +msgstr "" + #. Relevant view: Classic #. Lite view equivalent: "Add Meaning" #. Small button to add another sense/meaning for a word @@ -185,6 +221,13 @@ msgstr "Tambahkan bagian dari" msgid "Add Sense" msgstr "Tambahkan Pengertian" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning to this word" +#. Button on a possible-duplicate result in the New Entry dialog: saves the sense being typed onto that existing entry (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense to this entry" +msgstr "" + #. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state) #. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text #: src/project/browse/EntriesList.svelte @@ -374,6 +417,20 @@ msgstr "Perubahan juga dapat terjadi karena adanya penambahan field pada versi b msgid "Changing the same field twice, for example, can result in two commits but only one change that needs to be applied to FieldWorks Classic. Commits can also consist of multiple changes." msgstr "Mengubah bidang yang sama dua kali, misalnya, dapat menghasilkan dua komit namun hanya satu perubahan yang perlu diterapkan ke FieldWorks Classic. Commit juga dapat terdiri dari beberapa perubahan." +#. Relevant view: Classic +#. Lite view equivalent: "Checking for similar words…" +#. Status line in the New Entry dialog while searching for possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar entries…" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Checking for similar entries…" +#. Status line in the New Word dialog while searching for possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar words…" +msgstr "" + #. Status message while checking for updates #: src/lib/updates/UpdateDialogContent.svelte msgid "Checking for updates..." @@ -410,6 +467,7 @@ msgstr "jelas" #: src/lib/components/ui/button/x-button.svelte #: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte +#: src/lib/entry-editor/DuplicateSummaryPill.svelte #: src/project/tasks/SubjectPopup.svelte msgid "Close" msgstr "Tutup" @@ -1076,6 +1134,20 @@ msgstr "Arti Singkat" msgid "Go to {0}" msgstr "Buka {0}" +#. Relevant view: Classic +#. Lite view equivalent: "Go to word" +#. Tooltip on a possible-duplicate result in the New Entry dialog; clicking closes the dialog and opens that entry +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to entry" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Go to entry" +#. Tooltip on a possible-duplicate result in the New Word dialog; clicking closes the dialog and opens that word +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to word" +msgstr "" + #. Filter option label #. Relevant view: Classic #. Lite view equivalent: "Part of speech" @@ -1268,6 +1340,13 @@ msgstr "Login untuk melihat proyek" msgid "Logout" msgstr "Keluar" +#. Relevant view: Lite +#. Classic view equivalent: "No similar entries found" +#. Reassuring status line in the New Word dialog: the duplicate check found no existing words like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Looks like a new word" +msgstr "" + #. Footer text in the project sidebar. The emoji flags are the countries of origin of the development team — do not translate. #: src/project/ProjectSidebar.svelte msgid "Made with ❤️ from 🇦🇹 🇹🇭 🇺🇸" @@ -1313,6 +1392,13 @@ msgstr "Diperlukan pembaruan manual. Ikuti petunjuk yang diberikan." msgid "Meaning" msgstr "Arti" +#. Relevant view: Lite +#. Classic view equivalent: "Sense added to \"{0}\"" +#. Success notification after adding the typed meaning to an existing word; {0} is that word's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Meaning added to \"{0}\"" +msgstr "" + #. Task subject label in the task list. {0} = writing system abbreviation (e.g., "en", "fr"). Identifies entries lacking a definition in that writing system. #: src/project/tasks/tasks-service.ts msgid "Missing Definition {0}" @@ -1497,6 +1583,13 @@ msgstr "Tidak ada server" msgid "No server configured" msgstr "Tidak ada server yang dikonfigurasi" +#. Relevant view: Classic +#. Lite view equivalent: "Looks like a new word" +#. Status line in the New Entry dialog: the duplicate check found no existing entries like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "No similar entries found" +msgstr "" + #. Error message in the task editing drawer when no subject entity can be found. {0} = subject type (e.g., "sense", "example-sentence"). #: src/project/tasks/SubjectPopup.svelte msgid "No subject, unable to create a new {0}" @@ -1787,6 +1880,28 @@ msgstr "Ulasan" msgid "rose" msgstr "naik" +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: its headword is identical to the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same headword" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: only its lexeme form matches the typed text, while its citation form (shown as the headword) differs — deliberately NOT claiming "Same headword" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same lexeme form" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Same headword" +#. Badge on a possible-duplicate result: it is spelled exactly like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same word" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Save" msgstr "" @@ -1877,6 +1992,13 @@ msgstr "Kirimkan pesan kepada kami" msgid "Sense" msgstr "Pengertian" +#. Relevant view: Classic +#. Lite view equivalent: "Meaning added to \"{0}\"" +#. Success notification after adding the typed sense to an existing entry; {0} is that entry's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Sense added to \"{0}\"" +msgstr "" + #. Field label for example text #. The sentence/phrase text for an example (source text in original language) #. Paired with "Translation" field for translated version @@ -1915,6 +2037,7 @@ msgid "Show" msgstr "Tampilkan" #. Expand button with count +#: src/lib/entry-editor/DuplicateCheck.svelte #: src/lib/entry-editor/EntryOrSensePicker.svelte msgid "Show {0} more..." msgstr "Tampilkan {0} selengkapnya..." @@ -1923,6 +2046,48 @@ msgstr "Tampilkan {0} selengkapnya..." msgid "Sign in to add comments." msgstr "" +#. Relevant view: Classic +#. Lite view equivalent: "Similar words already exist" +#. Warning banner in the New Entry dialog; expands to a list of possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar entries already exist" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar meaning" +#. Badge on a possible-duplicate result: one of its glosses matches the gloss being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar gloss" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar word" +#. Badge on a possible-duplicate result: its headword partly matches the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar headword" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar gloss" +#. Badge on a possible-duplicate result: one of its meanings matches the meaning being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar meaning" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar headword" +#. Badge on a possible-duplicate result: it is spelled almost like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar word" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar entries already exist" +#. Warning banner in the New Word dialog; expands to a list of possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar words already exist" +msgstr "" + #. View option #: src/project/browse/EntryListViewOptions.svelte msgid "Simple" @@ -2120,6 +2285,13 @@ msgstr "{0} ini telah dihapus" msgid "This date # and this emoji # are snippets" msgstr "Tanggal ini # dan emoji ini # adalah cuplikan" +#. Relevant view: Classic +#. Lite view equivalent: "This word may already exist" +#. Warning banner in the New Entry dialog when an entry with the exact same headword exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This entry may already exist" +msgstr "" + #. Help message #: src/lib/components/OpenInFieldWorksButton.svelte msgid "This project is now open in FieldWorks. To continue working in FieldWorks Lite, close the project in FieldWorks and click Reopen." @@ -2135,6 +2307,13 @@ msgstr "Tugas ini selesai" msgid "This will synchronize the FieldWorks Lite and FieldWorks Classic copies of your project in Lexbox." msgstr "Ini akan menyinkronkan salinan FieldWorks Lite dan FieldWorks Classic dari proyek Anda di Lexbox." +#. Relevant view: Lite +#. Classic view equivalent: "This entry may already exist" +#. Warning banner in the New Word dialog when a word with the exact same spelling exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This word may already exist" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Thread" msgstr "" diff --git a/frontend/viewer/src/locales/ko.po b/frontend/viewer/src/locales/ko.po index fd2352e74c..63a0194f93 100644 --- a/frontend/viewer/src/locales/ko.po +++ b/frontend/viewer/src/locales/ko.po @@ -25,6 +25,7 @@ msgstr "" msgid "– ({0} changes)" msgstr "- ({0}개 변경)" +#: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte msgid "(unknown)" msgstr "" @@ -106,6 +107,20 @@ msgstr "**간단한 용어 사용** (예: *어휘 형태* 대신 *단어*, *의 msgid "A new version of FieldWorks Lite is available." msgstr "새 버전의 FieldWorks Lite를 사용할 수 있습니다." +#. Relevant view: Classic +#. Lite view equivalent: "A similar word already exists" +#. Warning banner in the New Entry dialog when exactly one possible duplicate was found (none an exact headword match) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar entry already exists" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "A similar entry already exists" +#. Warning banner in the New Word dialog when exactly one possible duplicate was found (none spelled exactly the same) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar word already exists" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "an entry" #: src/project/browse/BrowseView.svelte @@ -156,6 +171,13 @@ msgstr "구성 요소 추가" msgid "Add Example" msgstr "예제 추가" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense" +#. Short button label on a possible-duplicate result in the New Word dialog; fuller form: "Add meaning to this word" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning" +msgstr "" + #. Button label #. Relevant view: Lite #. Classic view equivalent: "Add Sense" @@ -165,6 +187,13 @@ msgstr "예제 추가" msgid "Add Meaning" msgstr "의미 추가" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense to this entry" +#. Button on a possible-duplicate result in the New Word dialog: saves the meaning being typed onto that existing word (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning to this word" +msgstr "" + #. Button in toolbar #: src/lib/entry-editor/NewEntryButton.svelte msgid "Add new word" @@ -177,6 +206,13 @@ msgstr "새 단어 추가" msgid "Add part of" msgstr "의 일부를 추가합니다." +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning" +#. Short button label on a possible-duplicate result in the New Entry dialog; fuller form: "Add sense to this entry" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense" +msgstr "" + #. Relevant view: Classic #. Lite view equivalent: "Add Meaning" #. Small button to add another sense/meaning for a word @@ -185,6 +221,13 @@ msgstr "의 일부를 추가합니다." msgid "Add Sense" msgstr "센스 추가" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning to this word" +#. Button on a possible-duplicate result in the New Entry dialog: saves the sense being typed onto that existing entry (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense to this entry" +msgstr "" + #. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state) #. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text #: src/project/browse/EntriesList.svelte @@ -374,6 +417,20 @@ msgstr "FieldWorks Lite의 새 버전에 필드가 추가되어 변경 사항이 msgid "Changing the same field twice, for example, can result in two commits but only one change that needs to be applied to FieldWorks Classic. Commits can also consist of multiple changes." msgstr "예를 들어 동일한 필드를 두 번 변경하면 두 개의 커밋이 발생하지만 FieldWorks Classic에 적용해야 하는 변경 사항은 하나만 발생할 수 있습니다. 커밋은 여러 변경 사항으로 구성될 수도 있습니다." +#. Relevant view: Classic +#. Lite view equivalent: "Checking for similar words…" +#. Status line in the New Entry dialog while searching for possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar entries…" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Checking for similar entries…" +#. Status line in the New Word dialog while searching for possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar words…" +msgstr "" + #. Status message while checking for updates #: src/lib/updates/UpdateDialogContent.svelte msgid "Checking for updates..." @@ -410,6 +467,7 @@ msgstr "clear" #: src/lib/components/ui/button/x-button.svelte #: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte +#: src/lib/entry-editor/DuplicateSummaryPill.svelte #: src/project/tasks/SubjectPopup.svelte msgid "Close" msgstr "닫기" @@ -1076,6 +1134,20 @@ msgstr "광택" msgid "Go to {0}" msgstr "{0}으로 이동" +#. Relevant view: Classic +#. Lite view equivalent: "Go to word" +#. Tooltip on a possible-duplicate result in the New Entry dialog; clicking closes the dialog and opens that entry +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to entry" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Go to entry" +#. Tooltip on a possible-duplicate result in the New Word dialog; clicking closes the dialog and opens that word +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to word" +msgstr "" + #. Filter option label #. Relevant view: Classic #. Lite view equivalent: "Part of speech" @@ -1268,6 +1340,13 @@ msgstr "로그인하여 프로젝트 보기" msgid "Logout" msgstr "로그아웃" +#. Relevant view: Lite +#. Classic view equivalent: "No similar entries found" +#. Reassuring status line in the New Word dialog: the duplicate check found no existing words like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Looks like a new word" +msgstr "" + #. Footer text in the project sidebar. The emoji flags are the countries of origin of the development team — do not translate. #: src/project/ProjectSidebar.svelte msgid "Made with ❤️ from 🇦🇹 🇹🇭 🇺🇸" @@ -1313,6 +1392,13 @@ msgstr "수동 업데이트가 필요합니다. 제공된 지침을 따르세요 msgid "Meaning" msgstr "의미" +#. Relevant view: Lite +#. Classic view equivalent: "Sense added to \"{0}\"" +#. Success notification after adding the typed meaning to an existing word; {0} is that word's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Meaning added to \"{0}\"" +msgstr "" + #. Task subject label in the task list. {0} = writing system abbreviation (e.g., "en", "fr"). Identifies entries lacking a definition in that writing system. #: src/project/tasks/tasks-service.ts msgid "Missing Definition {0}" @@ -1497,6 +1583,13 @@ msgstr "서버 없음" msgid "No server configured" msgstr "구성된 서버 없음" +#. Relevant view: Classic +#. Lite view equivalent: "Looks like a new word" +#. Status line in the New Entry dialog: the duplicate check found no existing entries like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "No similar entries found" +msgstr "" + #. Error message in the task editing drawer when no subject entity can be found. {0} = subject type (e.g., "sense", "example-sentence"). #: src/project/tasks/SubjectPopup.svelte msgid "No subject, unable to create a new {0}" @@ -1787,6 +1880,28 @@ msgstr "검토" msgid "rose" msgstr "rose" +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: its headword is identical to the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same headword" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: only its lexeme form matches the typed text, while its citation form (shown as the headword) differs — deliberately NOT claiming "Same headword" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same lexeme form" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Same headword" +#. Badge on a possible-duplicate result: it is spelled exactly like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same word" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Save" msgstr "" @@ -1877,6 +1992,13 @@ msgstr "메시지 보내기" msgid "Sense" msgstr "Sense" +#. Relevant view: Classic +#. Lite view equivalent: "Meaning added to \"{0}\"" +#. Success notification after adding the typed sense to an existing entry; {0} is that entry's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Sense added to \"{0}\"" +msgstr "" + #. Field label for example text #. The sentence/phrase text for an example (source text in original language) #. Paired with "Translation" field for translated version @@ -1915,6 +2037,7 @@ msgid "Show" msgstr "표시" #. Expand button with count +#: src/lib/entry-editor/DuplicateCheck.svelte #: src/lib/entry-editor/EntryOrSensePicker.svelte msgid "Show {0} more..." msgstr "보기 {0} 더보기..." @@ -1923,6 +2046,48 @@ msgstr "보기 {0} 더보기..." msgid "Sign in to add comments." msgstr "" +#. Relevant view: Classic +#. Lite view equivalent: "Similar words already exist" +#. Warning banner in the New Entry dialog; expands to a list of possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar entries already exist" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar meaning" +#. Badge on a possible-duplicate result: one of its glosses matches the gloss being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar gloss" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar word" +#. Badge on a possible-duplicate result: its headword partly matches the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar headword" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar gloss" +#. Badge on a possible-duplicate result: one of its meanings matches the meaning being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar meaning" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar headword" +#. Badge on a possible-duplicate result: it is spelled almost like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar word" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar entries already exist" +#. Warning banner in the New Word dialog; expands to a list of possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar words already exist" +msgstr "" + #. View option #: src/project/browse/EntryListViewOptions.svelte msgid "Simple" @@ -2120,6 +2285,13 @@ msgstr "이 {0} 삭제됨" msgid "This date # and this emoji # are snippets" msgstr "이 날짜 #와 이 이모티콘 #은 스니펫입니다." +#. Relevant view: Classic +#. Lite view equivalent: "This word may already exist" +#. Warning banner in the New Entry dialog when an entry with the exact same headword exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This entry may already exist" +msgstr "" + #. Help message #: src/lib/components/OpenInFieldWorksButton.svelte msgid "This project is now open in FieldWorks. To continue working in FieldWorks Lite, close the project in FieldWorks and click Reopen." @@ -2135,6 +2307,13 @@ msgstr "이 작업이 완료되었습니다." msgid "This will synchronize the FieldWorks Lite and FieldWorks Classic copies of your project in Lexbox." msgstr "이렇게 하면 렉스박스에서 프로젝트의 FieldWorks Lite 및 FieldWorks Classic 사본이 동기화됩니다." +#. Relevant view: Lite +#. Classic view equivalent: "This entry may already exist" +#. Warning banner in the New Word dialog when a word with the exact same spelling exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This word may already exist" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Thread" msgstr "" diff --git a/frontend/viewer/src/locales/ms.po b/frontend/viewer/src/locales/ms.po index 240778ebdf..d759e3293a 100644 --- a/frontend/viewer/src/locales/ms.po +++ b/frontend/viewer/src/locales/ms.po @@ -25,6 +25,7 @@ msgstr "" msgid "– ({0} changes)" msgstr "– ({0} perubahan)" +#: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte msgid "(unknown)" msgstr "" @@ -106,6 +107,20 @@ msgstr "**Terminologi lebih mudah** (cth. *Perkataan* bukan *bentuk leksem*, *Ma msgid "A new version of FieldWorks Lite is available." msgstr "Versi baharu FieldWorks Lite tersedia." +#. Relevant view: Classic +#. Lite view equivalent: "A similar word already exists" +#. Warning banner in the New Entry dialog when exactly one possible duplicate was found (none an exact headword match) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar entry already exists" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "A similar entry already exists" +#. Warning banner in the New Word dialog when exactly one possible duplicate was found (none spelled exactly the same) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar word already exists" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "an entry" #: src/project/browse/BrowseView.svelte @@ -156,6 +171,13 @@ msgstr "Tambah komponen" msgid "Add Example" msgstr "Tambah Contoh" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense" +#. Short button label on a possible-duplicate result in the New Word dialog; fuller form: "Add meaning to this word" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning" +msgstr "" + #. Button label #. Relevant view: Lite #. Classic view equivalent: "Add Sense" @@ -165,6 +187,13 @@ msgstr "Tambah Contoh" msgid "Add Meaning" msgstr "Tambah Makna" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense to this entry" +#. Button on a possible-duplicate result in the New Word dialog: saves the meaning being typed onto that existing word (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning to this word" +msgstr "" + #. Button in toolbar #: src/lib/entry-editor/NewEntryButton.svelte msgid "Add new word" @@ -177,6 +206,13 @@ msgstr "Tambah perkataan baharu" msgid "Add part of" msgstr "Tambah sebahagian daripada" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning" +#. Short button label on a possible-duplicate result in the New Entry dialog; fuller form: "Add sense to this entry" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense" +msgstr "" + #. Relevant view: Classic #. Lite view equivalent: "Add Meaning" #. Small button to add another sense/meaning for a word @@ -185,6 +221,13 @@ msgstr "Tambah sebahagian daripada" msgid "Add Sense" msgstr "Tambah Makna" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning to this word" +#. Button on a possible-duplicate result in the New Entry dialog: saves the sense being typed onto that existing entry (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense to this entry" +msgstr "" + #. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state) #. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text #: src/project/browse/EntriesList.svelte @@ -374,6 +417,20 @@ msgstr "Perubahan juga boleh menjadi hasil daripada medan yang ditambahkan ke ve msgid "Changing the same field twice, for example, can result in two commits but only one change that needs to be applied to FieldWorks Classic. Commits can also consist of multiple changes." msgstr "Mengubah medan yang sama dua kali, sebagai contoh, boleh menghasilkan dua komit tetapi hanya satu perubahan yang perlu digunakan pada FieldWorks Classic. Komit juga boleh terdiri daripada berbilang perubahan." +#. Relevant view: Classic +#. Lite view equivalent: "Checking for similar words…" +#. Status line in the New Entry dialog while searching for possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar entries…" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Checking for similar entries…" +#. Status line in the New Word dialog while searching for possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar words…" +msgstr "" + #. Status message while checking for updates #: src/lib/updates/UpdateDialogContent.svelte msgid "Checking for updates..." @@ -410,6 +467,7 @@ msgstr "padam" #: src/lib/components/ui/button/x-button.svelte #: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte +#: src/lib/entry-editor/DuplicateSummaryPill.svelte #: src/project/tasks/SubjectPopup.svelte msgid "Close" msgstr "Tutup" @@ -1076,6 +1134,20 @@ msgstr "Glos" msgid "Go to {0}" msgstr "Pergi ke {0}" +#. Relevant view: Classic +#. Lite view equivalent: "Go to word" +#. Tooltip on a possible-duplicate result in the New Entry dialog; clicking closes the dialog and opens that entry +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to entry" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Go to entry" +#. Tooltip on a possible-duplicate result in the New Word dialog; clicking closes the dialog and opens that word +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to word" +msgstr "" + #. Filter option label #. Relevant view: Classic #. Lite view equivalent: "Part of speech" @@ -1268,6 +1340,13 @@ msgstr "Log masuk untuk melihat projek" msgid "Logout" msgstr "Log keluar" +#. Relevant view: Lite +#. Classic view equivalent: "No similar entries found" +#. Reassuring status line in the New Word dialog: the duplicate check found no existing words like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Looks like a new word" +msgstr "" + #. Footer text in the project sidebar. The emoji flags are the countries of origin of the development team — do not translate. #: src/project/ProjectSidebar.svelte msgid "Made with ❤️ from 🇦🇹 🇹🇭 🇺🇸" @@ -1313,6 +1392,13 @@ msgstr "Kemas kini manual diperlukan. Sila ikut arahan yang diberikan." msgid "Meaning" msgstr "Makna" +#. Relevant view: Lite +#. Classic view equivalent: "Sense added to \"{0}\"" +#. Success notification after adding the typed meaning to an existing word; {0} is that word's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Meaning added to \"{0}\"" +msgstr "" + #. Task subject label in the task list. {0} = writing system abbreviation (e.g., "en", "fr"). Identifies entries lacking a definition in that writing system. #: src/project/tasks/tasks-service.ts msgid "Missing Definition {0}" @@ -1497,6 +1583,13 @@ msgstr "Tiada pelayan" msgid "No server configured" msgstr "Tiada pelayan dikonfigurasikan" +#. Relevant view: Classic +#. Lite view equivalent: "Looks like a new word" +#. Status line in the New Entry dialog: the duplicate check found no existing entries like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "No similar entries found" +msgstr "" + #. Error message in the task editing drawer when no subject entity can be found. {0} = subject type (e.g., "sense", "example-sentence"). #: src/project/tasks/SubjectPopup.svelte msgid "No subject, unable to create a new {0}" @@ -1787,6 +1880,28 @@ msgstr "Semak" msgid "rose" msgstr "merah jambu" +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: its headword is identical to the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same headword" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: only its lexeme form matches the typed text, while its citation form (shown as the headword) differs — deliberately NOT claiming "Same headword" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same lexeme form" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Same headword" +#. Badge on a possible-duplicate result: it is spelled exactly like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same word" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Save" msgstr "" @@ -1877,6 +1992,13 @@ msgstr "Hantar kami mesej" msgid "Sense" msgstr "Makna" +#. Relevant view: Classic +#. Lite view equivalent: "Meaning added to \"{0}\"" +#. Success notification after adding the typed sense to an existing entry; {0} is that entry's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Sense added to \"{0}\"" +msgstr "" + #. Field label for example text #. The sentence/phrase text for an example (source text in original language) #. Paired with "Translation" field for translated version @@ -1915,6 +2037,7 @@ msgid "Show" msgstr "Tunjukkan" #. Expand button with count +#: src/lib/entry-editor/DuplicateCheck.svelte #: src/lib/entry-editor/EntryOrSensePicker.svelte msgid "Show {0} more..." msgstr "Tunjukkan {0} lagi..." @@ -1923,6 +2046,48 @@ msgstr "Tunjukkan {0} lagi..." msgid "Sign in to add comments." msgstr "" +#. Relevant view: Classic +#. Lite view equivalent: "Similar words already exist" +#. Warning banner in the New Entry dialog; expands to a list of possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar entries already exist" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar meaning" +#. Badge on a possible-duplicate result: one of its glosses matches the gloss being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar gloss" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar word" +#. Badge on a possible-duplicate result: its headword partly matches the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar headword" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar gloss" +#. Badge on a possible-duplicate result: one of its meanings matches the meaning being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar meaning" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar headword" +#. Badge on a possible-duplicate result: it is spelled almost like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar word" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar entries already exist" +#. Warning banner in the New Word dialog; expands to a list of possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar words already exist" +msgstr "" + #. View option #: src/project/browse/EntryListViewOptions.svelte msgid "Simple" @@ -2120,6 +2285,13 @@ msgstr "Item {0} telah dipadam" msgid "This date # and this emoji # are snippets" msgstr "Tarikh ini # dan emoji ini # adalah coretan" +#. Relevant view: Classic +#. Lite view equivalent: "This word may already exist" +#. Warning banner in the New Entry dialog when an entry with the exact same headword exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This entry may already exist" +msgstr "" + #. Help message #: src/lib/components/OpenInFieldWorksButton.svelte msgid "This project is now open in FieldWorks. To continue working in FieldWorks Lite, close the project in FieldWorks and click Reopen." @@ -2135,6 +2307,13 @@ msgstr "Tugasan ini selesai" msgid "This will synchronize the FieldWorks Lite and FieldWorks Classic copies of your project in Lexbox." msgstr "Ini akan menyegerakkan salinan FieldWorks Lite dan FieldWorks Classic projek anda dalam Lexbox." +#. Relevant view: Lite +#. Classic view equivalent: "This entry may already exist" +#. Warning banner in the New Word dialog when a word with the exact same spelling exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This word may already exist" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Thread" msgstr "" diff --git a/frontend/viewer/src/locales/sw.po b/frontend/viewer/src/locales/sw.po index 0960d285ec..3c09fdd3de 100644 --- a/frontend/viewer/src/locales/sw.po +++ b/frontend/viewer/src/locales/sw.po @@ -25,6 +25,7 @@ msgstr "" msgid "– ({0} changes)" msgstr "– ({0} mabadiliko)" +#: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte msgid "(unknown)" msgstr "" @@ -106,6 +107,20 @@ msgstr "**Istilahi rahisi** (kwa mfano *Neno* badala ya *Lexeme form*, *Maana* b msgid "A new version of FieldWorks Lite is available." msgstr "Toleo jipya la FieldWorks Lite linapatikana." +#. Relevant view: Classic +#. Lite view equivalent: "A similar word already exists" +#. Warning banner in the New Entry dialog when exactly one possible duplicate was found (none an exact headword match) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar entry already exists" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "A similar entry already exists" +#. Warning banner in the New Word dialog when exactly one possible duplicate was found (none spelled exactly the same) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar word already exists" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "an entry" #: src/project/browse/BrowseView.svelte @@ -156,6 +171,13 @@ msgstr "Ongeza sehemu" msgid "Add Example" msgstr "Ongeza Mfano" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense" +#. Short button label on a possible-duplicate result in the New Word dialog; fuller form: "Add meaning to this word" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning" +msgstr "" + #. Button label #. Relevant view: Lite #. Classic view equivalent: "Add Sense" @@ -165,6 +187,13 @@ msgstr "Ongeza Mfano" msgid "Add Meaning" msgstr "Ongeza Maana" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense to this entry" +#. Button on a possible-duplicate result in the New Word dialog: saves the meaning being typed onto that existing word (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning to this word" +msgstr "" + #. Button in toolbar #: src/lib/entry-editor/NewEntryButton.svelte msgid "Add new word" @@ -177,6 +206,13 @@ msgstr "Ongeza neno jipya" msgid "Add part of" msgstr "Ongeza sehemu ya" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning" +#. Short button label on a possible-duplicate result in the New Entry dialog; fuller form: "Add sense to this entry" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense" +msgstr "" + #. Relevant view: Classic #. Lite view equivalent: "Add Meaning" #. Small button to add another sense/meaning for a word @@ -185,6 +221,13 @@ msgstr "Ongeza sehemu ya" msgid "Add Sense" msgstr "Ongeza Maana" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning to this word" +#. Button on a possible-duplicate result in the New Entry dialog: saves the sense being typed onto that existing entry (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense to this entry" +msgstr "" + #. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state) #. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text #: src/project/browse/EntriesList.svelte @@ -374,6 +417,20 @@ msgstr "Mabadiliko yanaweza pia kuwa matokeo ya sehemu kuongezwa kwa matoleo map msgid "Changing the same field twice, for example, can result in two commits but only one change that needs to be applied to FieldWorks Classic. Commits can also consist of multiple changes." msgstr "Kubadilisha sehemu sawa mara mbili, kwa mfano, kunaweza kusababisha kumbukas mbili lakini badiliko moja tu linayohitaji kutumwa kwa FieldWorks Classic. Kumbukas kunaweza pia kuwa na mabadiliko mengi." +#. Relevant view: Classic +#. Lite view equivalent: "Checking for similar words…" +#. Status line in the New Entry dialog while searching for possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar entries…" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Checking for similar entries…" +#. Status line in the New Word dialog while searching for possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar words…" +msgstr "" + #. Status message while checking for updates #: src/lib/updates/UpdateDialogContent.svelte msgid "Checking for updates..." @@ -410,6 +467,7 @@ msgstr "futa" #: src/lib/components/ui/button/x-button.svelte #: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte +#: src/lib/entry-editor/DuplicateSummaryPill.svelte #: src/project/tasks/SubjectPopup.svelte msgid "Close" msgstr "Funga" @@ -1076,6 +1134,20 @@ msgstr "Glosi" msgid "Go to {0}" msgstr "Nenda kwa {0}" +#. Relevant view: Classic +#. Lite view equivalent: "Go to word" +#. Tooltip on a possible-duplicate result in the New Entry dialog; clicking closes the dialog and opens that entry +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to entry" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Go to entry" +#. Tooltip on a possible-duplicate result in the New Word dialog; clicking closes the dialog and opens that word +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to word" +msgstr "" + #. Filter option label #. Relevant view: Classic #. Lite view equivalent: "Part of speech" @@ -1268,6 +1340,13 @@ msgstr "Ingia kuona miradi" msgid "Logout" msgstr "Toka nje" +#. Relevant view: Lite +#. Classic view equivalent: "No similar entries found" +#. Reassuring status line in the New Word dialog: the duplicate check found no existing words like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Looks like a new word" +msgstr "" + #. Footer text in the project sidebar. The emoji flags are the countries of origin of the development team — do not translate. #: src/project/ProjectSidebar.svelte msgid "Made with ❤️ from 🇦🇹 🇹🇭 🇺🇸" @@ -1313,6 +1392,13 @@ msgstr "Sasisho la mwongozo linahitajika. Tafadhali fuata maelekezo yaliyotolewa msgid "Meaning" msgstr "Maana" +#. Relevant view: Lite +#. Classic view equivalent: "Sense added to \"{0}\"" +#. Success notification after adding the typed meaning to an existing word; {0} is that word's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Meaning added to \"{0}\"" +msgstr "" + #. Task subject label in the task list. {0} = writing system abbreviation (e.g., "en", "fr"). Identifies entries lacking a definition in that writing system. #: src/project/tasks/tasks-service.ts msgid "Missing Definition {0}" @@ -1497,6 +1583,13 @@ msgstr "Hakuna seva" msgid "No server configured" msgstr "Hakuna seva iliyotengwa" +#. Relevant view: Classic +#. Lite view equivalent: "Looks like a new word" +#. Status line in the New Entry dialog: the duplicate check found no existing entries like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "No similar entries found" +msgstr "" + #. Error message in the task editing drawer when no subject entity can be found. {0} = subject type (e.g., "sense", "example-sentence"). #: src/project/tasks/SubjectPopup.svelte msgid "No subject, unable to create a new {0}" @@ -1787,6 +1880,28 @@ msgstr "Tathmini" msgid "rose" msgstr "waridi" +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: its headword is identical to the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same headword" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: only its lexeme form matches the typed text, while its citation form (shown as the headword) differs — deliberately NOT claiming "Same headword" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same lexeme form" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Same headword" +#. Badge on a possible-duplicate result: it is spelled exactly like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same word" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Save" msgstr "" @@ -1877,6 +1992,13 @@ msgstr "Tuambii ujumbe" msgid "Sense" msgstr "Kwa maana" +#. Relevant view: Classic +#. Lite view equivalent: "Meaning added to \"{0}\"" +#. Success notification after adding the typed sense to an existing entry; {0} is that entry's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Sense added to \"{0}\"" +msgstr "" + #. Field label for example text #. The sentence/phrase text for an example (source text in original language) #. Paired with "Translation" field for translated version @@ -1915,6 +2037,7 @@ msgid "Show" msgstr "Onyesha" #. Expand button with count +#: src/lib/entry-editor/DuplicateCheck.svelte #: src/lib/entry-editor/EntryOrSensePicker.svelte msgid "Show {0} more..." msgstr "Onyesha {0} zaidi..." @@ -1923,6 +2046,48 @@ msgstr "Onyesha {0} zaidi..." msgid "Sign in to add comments." msgstr "" +#. Relevant view: Classic +#. Lite view equivalent: "Similar words already exist" +#. Warning banner in the New Entry dialog; expands to a list of possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar entries already exist" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar meaning" +#. Badge on a possible-duplicate result: one of its glosses matches the gloss being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar gloss" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar word" +#. Badge on a possible-duplicate result: its headword partly matches the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar headword" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar gloss" +#. Badge on a possible-duplicate result: one of its meanings matches the meaning being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar meaning" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar headword" +#. Badge on a possible-duplicate result: it is spelled almost like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar word" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar entries already exist" +#. Warning banner in the New Word dialog; expands to a list of possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar words already exist" +msgstr "" + #. View option #: src/project/browse/EntryListViewOptions.svelte msgid "Simple" @@ -2120,6 +2285,13 @@ msgstr "Hili {0} limefutwa" msgid "This date # and this emoji # are snippets" msgstr "Tarehe hii # na emoji hii # ni vigezo" +#. Relevant view: Classic +#. Lite view equivalent: "This word may already exist" +#. Warning banner in the New Entry dialog when an entry with the exact same headword exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This entry may already exist" +msgstr "" + #. Help message #: src/lib/components/OpenInFieldWorksButton.svelte msgid "This project is now open in FieldWorks. To continue working in FieldWorks Lite, close the project in FieldWorks and click Reopen." @@ -2135,6 +2307,13 @@ msgstr "Jukumu hili limekamilika" msgid "This will synchronize the FieldWorks Lite and FieldWorks Classic copies of your project in Lexbox." msgstr "Hii itaoanisha kopia za FieldWorks Lite na FieldWorks Classic za mradi wako kwenye Lexbox." +#. Relevant view: Lite +#. Classic view equivalent: "This entry may already exist" +#. Warning banner in the New Word dialog when a word with the exact same spelling exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This word may already exist" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Thread" msgstr "" diff --git a/frontend/viewer/src/locales/vi.po b/frontend/viewer/src/locales/vi.po index 0e1901b11b..2718881d9b 100644 --- a/frontend/viewer/src/locales/vi.po +++ b/frontend/viewer/src/locales/vi.po @@ -25,6 +25,7 @@ msgstr "" msgid "– ({0} changes)" msgstr "– ({0} thay đổi)" +#: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte msgid "(unknown)" msgstr "" @@ -106,6 +107,20 @@ msgstr "**Thuật ngữ đơn giản hơn** (ví dụ *Từ* thay vì *Hình th msgid "A new version of FieldWorks Lite is available." msgstr "Đã có phiên bản mới của FieldWorks Lite." +#. Relevant view: Classic +#. Lite view equivalent: "A similar word already exists" +#. Warning banner in the New Entry dialog when exactly one possible duplicate was found (none an exact headword match) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar entry already exists" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "A similar entry already exists" +#. Warning banner in the New Word dialog when exactly one possible duplicate was found (none spelled exactly the same) +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "A similar word already exists" +msgstr "" + #. Relevant view: Lite #. Classic view equivalent: "an entry" #: src/project/browse/BrowseView.svelte @@ -156,6 +171,13 @@ msgstr "Thêm thành phần" msgid "Add Example" msgstr "Thêm ví dụ" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense" +#. Short button label on a possible-duplicate result in the New Word dialog; fuller form: "Add meaning to this word" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning" +msgstr "" + #. Button label #. Relevant view: Lite #. Classic view equivalent: "Add Sense" @@ -165,6 +187,13 @@ msgstr "Thêm ví dụ" msgid "Add Meaning" msgstr "Thêm Ý nghĩa" +#. Relevant view: Lite +#. Classic view equivalent: "Add sense to this entry" +#. Button on a possible-duplicate result in the New Word dialog: saves the meaning being typed onto that existing word (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add meaning to this word" +msgstr "" + #. Button in toolbar #: src/lib/entry-editor/NewEntryButton.svelte msgid "Add new word" @@ -177,6 +206,13 @@ msgstr "Thêm từ mới" msgid "Add part of" msgstr "Thêm phần của" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning" +#. Short button label on a possible-duplicate result in the New Entry dialog; fuller form: "Add sense to this entry" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense" +msgstr "" + #. Relevant view: Classic #. Lite view equivalent: "Add Meaning" #. Small button to add another sense/meaning for a word @@ -185,6 +221,13 @@ msgstr "Thêm phần của" msgid "Add Sense" msgstr "Thêm Ý nghĩa (sense)" +#. Relevant view: Classic +#. Lite view equivalent: "Add meaning to this word" +#. Button on a possible-duplicate result in the New Entry dialog: saves the sense being typed onto that existing entry (instead of creating a duplicate) and opens it +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Add sense to this entry" +msgstr "" + #. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state) #. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text #: src/project/browse/EntriesList.svelte @@ -374,6 +417,20 @@ msgstr "Các thay đổi cũng có thể do các trường mới được thêm msgid "Changing the same field twice, for example, can result in two commits but only one change that needs to be applied to FieldWorks Classic. Commits can also consist of multiple changes." msgstr "Thay đổi cùng một trường hai lần, ví dụ, có thể dẫn đến hai commit nhưng chỉ một thay đổi cần được áp dụng cho FieldWorks Classic. Commit cũng có thể gồm nhiều thay đổi." +#. Relevant view: Classic +#. Lite view equivalent: "Checking for similar words…" +#. Status line in the New Entry dialog while searching for possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar entries…" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Checking for similar entries…" +#. Status line in the New Word dialog while searching for possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Checking for similar words…" +msgstr "" + #. Status message while checking for updates #: src/lib/updates/UpdateDialogContent.svelte msgid "Checking for updates..." @@ -410,6 +467,7 @@ msgstr "xóa" #: src/lib/components/ui/button/x-button.svelte #: src/lib/entry-editor/CommentDialog.svelte #: src/lib/entry-editor/CommentDialog.svelte +#: src/lib/entry-editor/DuplicateSummaryPill.svelte #: src/project/tasks/SubjectPopup.svelte msgid "Close" msgstr "Đóng" @@ -1076,6 +1134,20 @@ msgstr "Ghi chú" msgid "Go to {0}" msgstr "Đi tới {0}" +#. Relevant view: Classic +#. Lite view equivalent: "Go to word" +#. Tooltip on a possible-duplicate result in the New Entry dialog; clicking closes the dialog and opens that entry +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to entry" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Go to entry" +#. Tooltip on a possible-duplicate result in the New Word dialog; clicking closes the dialog and opens that word +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Go to word" +msgstr "" + #. Filter option label #. Relevant view: Classic #. Lite view equivalent: "Part of speech" @@ -1268,6 +1340,13 @@ msgstr "Đăng nhập để xem dự án" msgid "Logout" msgstr "Đăng xuất" +#. Relevant view: Lite +#. Classic view equivalent: "No similar entries found" +#. Reassuring status line in the New Word dialog: the duplicate check found no existing words like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Looks like a new word" +msgstr "" + #. Footer text in the project sidebar. The emoji flags are the countries of origin of the development team — do not translate. #: src/project/ProjectSidebar.svelte msgid "Made with ❤️ from 🇦🇹 🇹🇭 🇺🇸" @@ -1313,6 +1392,13 @@ msgstr "Cần thực hiện cập nhật thủ công. Vui lòng làm theo các h msgid "Meaning" msgstr "Ý nghĩa" +#. Relevant view: Lite +#. Classic view equivalent: "Sense added to \"{0}\"" +#. Success notification after adding the typed meaning to an existing word; {0} is that word's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Meaning added to \"{0}\"" +msgstr "" + #. Task subject label in the task list. {0} = writing system abbreviation (e.g., "en", "fr"). Identifies entries lacking a definition in that writing system. #: src/project/tasks/tasks-service.ts msgid "Missing Definition {0}" @@ -1497,6 +1583,13 @@ msgstr "Không có máy chủ" msgid "No server configured" msgstr "Chưa cấu hình máy chủ" +#. Relevant view: Classic +#. Lite view equivalent: "Looks like a new word" +#. Status line in the New Entry dialog: the duplicate check found no existing entries like the one being typed +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "No similar entries found" +msgstr "" + #. Error message in the task editing drawer when no subject entity can be found. {0} = subject type (e.g., "sense", "example-sentence"). #: src/project/tasks/SubjectPopup.svelte msgid "No subject, unable to create a new {0}" @@ -1787,6 +1880,28 @@ msgstr "Xem lại" msgid "rose" msgstr "hoa hồng" +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: its headword is identical to the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same headword" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Same word" +#. Badge on a possible-duplicate result: only its lexeme form matches the typed text, while its citation form (shown as the headword) differs — deliberately NOT claiming "Same headword" +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same lexeme form" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Same headword" +#. Badge on a possible-duplicate result: it is spelled exactly like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Same word" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Save" msgstr "" @@ -1877,6 +1992,13 @@ msgstr "Gửi cho chúng tôi một tin nhắn" msgid "Sense" msgstr "Ý nghĩa (sense)" +#. Relevant view: Classic +#. Lite view equivalent: "Meaning added to \"{0}\"" +#. Success notification after adding the typed sense to an existing entry; {0} is that entry's headword +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Sense added to \"{0}\"" +msgstr "" + #. Field label for example text #. The sentence/phrase text for an example (source text in original language) #. Paired with "Translation" field for translated version @@ -1915,6 +2037,7 @@ msgid "Show" msgstr "Hiển thị" #. Expand button with count +#: src/lib/entry-editor/DuplicateCheck.svelte #: src/lib/entry-editor/EntryOrSensePicker.svelte msgid "Show {0} more..." msgstr "Hiển thị thêm {0}..." @@ -1923,6 +2046,48 @@ msgstr "Hiển thị thêm {0}..." msgid "Sign in to add comments." msgstr "" +#. Relevant view: Classic +#. Lite view equivalent: "Similar words already exist" +#. Warning banner in the New Entry dialog; expands to a list of possible duplicate entries +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar entries already exist" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar meaning" +#. Badge on a possible-duplicate result: one of its glosses matches the gloss being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar gloss" +msgstr "" + +#. Relevant view: Classic +#. Lite view equivalent: "Similar word" +#. Badge on a possible-duplicate result: its headword partly matches the one being typed in the New Entry dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar headword" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar gloss" +#. Badge on a possible-duplicate result: one of its meanings matches the meaning being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar meaning" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar headword" +#. Badge on a possible-duplicate result: it is spelled almost like the word being typed in the New Word dialog +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar word" +msgstr "" + +#. Relevant view: Lite +#. Classic view equivalent: "Similar entries already exist" +#. Warning banner in the New Word dialog; expands to a list of possible duplicate words +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "Similar words already exist" +msgstr "" + #. View option #: src/project/browse/EntryListViewOptions.svelte msgid "Simple" @@ -2120,6 +2285,13 @@ msgstr "Đường dẫn này {0} đã bị xóa." msgid "This date # and this emoji # are snippets" msgstr "Ngày này # và biểu tượng cảm xúc này # là đoạn trích" +#. Relevant view: Classic +#. Lite view equivalent: "This word may already exist" +#. Warning banner in the New Entry dialog when an entry with the exact same headword exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This entry may already exist" +msgstr "" + #. Help message #: src/lib/components/OpenInFieldWorksButton.svelte msgid "This project is now open in FieldWorks. To continue working in FieldWorks Lite, close the project in FieldWorks and click Reopen." @@ -2135,6 +2307,13 @@ msgstr "Nhiệm vụ này đã hoàn thành" msgid "This will synchronize the FieldWorks Lite and FieldWorks Classic copies of your project in Lexbox." msgstr "Điều này sẽ đồng bộ các bản sao FieldWorks Lite và FieldWorks Classic của dự án của bạn trong Lexbox." +#. Relevant view: Lite +#. Classic view equivalent: "This entry may already exist" +#. Warning banner in the New Word dialog when a word with the exact same spelling exists; expands to a list of possible duplicates +#: src/lib/entry-editor/DuplicateCheck.svelte +msgid "This word may already exist" +msgstr "" + #: src/lib/entry-editor/CommentDialog.svelte msgid "Thread" msgstr "" diff --git a/frontend/viewer/tests/new-entry-duplicates.test.ts b/frontend/viewer/tests/new-entry-duplicates.test.ts new file mode 100644 index 0000000000..205ed1f3dc --- /dev/null +++ b/frontend/viewer/tests/new-entry-duplicates.test.ts @@ -0,0 +1,166 @@ +import {expect, test, type Locator, type Page} from '@playwright/test'; +import {BrowsePage} from './browse-page'; + +/** + * Tests for the possible-duplicates check in the new entry dialog: + * typing an existing word/gloss surfaces similar entries, a brand-new word + * gets the "no similar entries" indicator, and a match row expands in place + * to offer "Go to entry" / "Add sense". + */ + +// demo data (see demo-entry-data.ts): entry 'baba' exists, glossed 'father' +const existingLexeme = 'baba'; +const existingGloss = 'father'; + +async function openNewEntryDialog(page: Page): Promise { + await page.getByRole('button', {name: /new (entry|word)/i}).first().click(); + const dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + return dialog; +} + +function lexemeInput(dialog: Locator): Locator { + return dialog.locator('[style*="grid-area: lexemeForm"]').locator('input').first(); +} + +function glossInput(dialog: Locator): Locator { + return dialog.locator('[style*="grid-area: gloss"]').locator('input').first(); +} + +const duplicatesSummary = /already exist/i; +const newWordIndicator = /no similar entries found|looks like a new word/i; + +/** The duplicate strip's header — .first() because the jump pill repeats the message when the strip is out of view. */ +function stripSummary(dialog: Locator): Locator { + return dialog.getByText(duplicatesSummary).first(); +} + +/** The visible match rows — each expands in place (aria-expanded) to reveal its actions; the collapsed strip keeps them in the DOM hidden. */ +function duplicateRows(dialog: Locator): Locator { + return dialog.locator('li > button[aria-expanded]:visible'); +} + +test.describe('New entry possible duplicates', () => { + test('typing an existing word shows duplicates and can navigate to one', async ({page}) => { + const browsePage = new BrowsePage(page); + await browsePage.goto(); + + const dialog = await openNewEntryDialog(page); + await lexemeInput(dialog).fill(existingLexeme); + + // exact headword match => attention strip + auto-expanded list + await expect(stripSummary(dialog)).toBeVisible(); + // 'baba' is a substring of 'ubaba', so both rows match — .first() is the exact match because same-word sorts first + const duplicateRow = duplicateRows(dialog).filter({hasText: existingLexeme}).first(); + await duplicateRow.click(); + await expect(duplicateRow).toHaveAttribute('aria-expanded', 'true'); + + await dialog.getByRole('button', {name: /go to (entry|word)/i}).click(); + await expect(dialog).toBeHidden(); + await expect(page).toHaveURL(/entryId=/); + const openedLexeme = await browsePage.entryView.getLexemeInput(); + await expect(openedLexeme).toHaveValue(existingLexeme); + }); + + test('brand-new word shows the new-word indicator', async ({page}) => { + const browsePage = new BrowsePage(page); + await browsePage.goto(); + + const dialog = await openNewEntryDialog(page); + await lexemeInput(dialog).fill('zyzzyvazz'); + await expect(dialog.getByText(newWordIndicator)).toBeVisible(); + await expect(stripSummary(dialog)).toBeHidden(); + }); + + test('typed meaning can be added to an existing entry instead', async ({page}) => { + const browsePage = new BrowsePage(page); + await browsePage.goto(); + + const dialog = await openNewEntryDialog(page); + await lexemeInput(dialog).fill(existingLexeme); + const newGloss = `rescued-${Date.now().toString().slice(-6)}`; + await glossInput(dialog).fill(newGloss); + + // exact match auto-expands the list; expand the row to reach its actions + const duplicateRow = duplicateRows(dialog).filter({hasText: existingLexeme}).first(); + await duplicateRow.click(); + await dialog.getByRole('button', {name: /add (sense|meaning)/i}).click(); + + await expect(dialog).toBeHidden(); + await expect(page).toHaveURL(/entryId=/); + const entryId = new URL(page.url()).searchParams.get('entryId'); + expect(entryId).toBeTruthy(); + await expect(async () => { + expect(await browsePage.api.entryHasGlossValue(entryId!, newGloss)).toBe(true); + }).toPass({timeout: 5000}); + }); + + test('partial headword match shows a collapsed strip with a similar-word badge', async ({page}) => { + const browsePage = new BrowsePage(page); + await browsePage.goto(); + + const dialog = await openNewEntryDialog(page); + // substring of 'balalika' only — no exact match, so the strip stays collapsed + await lexemeInput(dialog).fill('balal'); + + const summary = stripSummary(dialog); + await expect(summary).toBeVisible(); + await expect(duplicateRows(dialog)).toHaveCount(0); + await summary.click(); + await expect(dialog.getByText(/similar (headword|word)/i).first()).toBeVisible(); + }); + + test('long match lists collapse behind Show more and a capped count', async ({page}) => { + const browsePage = new BrowsePage(page); + await browsePage.goto(); + + const dialog = await openNewEntryDialog(page); + await lexemeInput(dialog).fill('ba'); + + const summary = stripSummary(dialog); + await expect(summary).toBeVisible(); + // dozens of demo lexemes contain 'ba', so the fetch cap is hit and the count renders as 'N+' + await expect(dialog.getByText(/^\d+\+$/).first()).toBeVisible(); + + const rows = duplicateRows(dialog); + if (await rows.count() === 0) await summary.click(); // expands automatically only on an exact match + // 3 = the component's initial display count + await expect(rows).toHaveCount(3); + await dialog.getByRole('button', {name: /show \d+ more/i}).click(); + expect(await rows.count()).toBeGreaterThan(3); + }); + + test('matching gloss shows duplicates with a meaning badge', async ({page}) => { + const browsePage = new BrowsePage(page); + await browsePage.goto(); + + const dialog = await openNewEntryDialog(page); + await lexemeInput(dialog).fill('zyzzyvazz'); + await glossInput(dialog).fill(existingGloss); + + const summary = stripSummary(dialog); + await expect(summary).toBeVisible(); + // gloss-only matches don't auto-expand; expand to see the badge + await summary.click(); + await expect(dialog.getByText(/similar (gloss|meaning)/i).first()).toBeVisible(); + }); + + test('an out-of-view duplicate strip surfaces a jump pill', async ({page}) => { + // small viewport so the duplicate strip (below the editor grid) starts outside the dialog's scroll view + await page.setViewportSize({width: 1024, height: 560}); + const browsePage = new BrowsePage(page); + await browsePage.goto(); + + const dialog = await openNewEntryDialog(page); + await lexemeInput(dialog).fill(existingLexeme); + + // the pill's accessible name is exactly the summary message; the strip trigger's name has more text + const pill = dialog.getByRole('button', {name: /^this (entry|word) may already exist$/i}); + await expect(pill).toBeVisible(); + await pill.click(); + + // jumping scrolls the strip into view, which dismisses the pill and shows the match rows + await expect(duplicateRows(dialog).first()).toBeInViewport(); + await expect(pill).toBeHidden(); + }); +});