cui/source/dialogs/SpellDialog.cxx  |   23 ++++++++++++++++++++---
 linguistic/source/dicimp.cxx        |   13 ++++++++++++-
 sw/source/uibase/lingu/olmenu.cxx   |   18 +++++++++++++++++-
 sw/source/uibase/shells/textsh1.cxx |   21 +++++++++++++++++----
 4 files changed, 66 insertions(+), 9 deletions(-)

New commits:
commit 80dcb39edbffbe53d6833160afb7ad11cc51409d
Author:     László Németh <[email protected]>
AuthorDate: Sun Dec 28 05:04:17 2025 +0100
Commit:     László Németh <[email protected]>
CommitDate: Sun Dec 28 23:48:55 2025 +0100

    tdf#130695 sw linguistic: fix custom abbreviations
    
    linguistic: Abbreviations in the custom dictionary
    were accepted without the terminating dot, too
    (because bSimilarOnly comparison used by getEntry()
    removes all terminating dots during the dictionary
    look up.)
    
    Because "Add to [custom] dictionary" always removed
    the terminating dot, and direct editing of the custom
    dictionaries is still not so comfortable, the following
    user-friendly options were implemented for the
    abbreviations:
    
    – spell checking dialog: words with multiple dots
      – e.g. F.A.C.S. –, are always added to the custom dictionary
      with the terminating dot to reject the bad abbreviations:
      "e.g", "F.A.C.S" etc.;
    
    – context menu: add optional menu items to the context menu
      of the abbreviations to put the word with the terminating
      dot into the custom dictionaries, e.g. for the word "Corp.":
    
      * standard.dic (Corp)
      * standard.dic (Corp.)
    
      as menu items of the the Add to Dictionary submenu.
    
    Change-Id: I54532c0a63af8effe9f103e75e853703d7d57563
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/196263
    Reviewed-by: László Németh <[email protected]>
    Tested-by: Jenkins

diff --git a/cui/source/dialogs/SpellDialog.cxx 
b/cui/source/dialogs/SpellDialog.cxx
index 56c089c347f7..357a8fb2830e 100644
--- a/cui/source/dialogs/SpellDialog.cxx
+++ b/cui/source/dialogs/SpellDialog.cxx
@@ -603,6 +603,23 @@ IMPL_LINK_NOARG(SpellDialog, ChangeHdl, weld::Button&, 
void)
         m_xIgnorePB->grab_focus();
 }
 
+// words with multiple dots are added to the custom
+// dictionary with the terminating dot
+static bool lcl_IsStripDot( const OUString & rWord )
+{
+    // strip the terminating dot, if there is no
+    // an extra dot inside the word, e.g.
+    // put "dots." as "dots" into the dictionary,
+    // but put "F.A.C.S." as "F.A.C.S.", not "F.A.C.S"
+    if ( rWord.endsWith(".") ) {
+        sal_Int32 nDotPos = rWord.indexOf(".");
+        if ( nDotPos == rWord.getLength() - 1 )
+            return true;
+    }
+
+    return false;
+}
+
 IMPL_LINK_NOARG(SpellDialog, ChangeAllHdl, weld::Button&, void)
 {
     auto xGuard(std::make_unique<UndoChangeGroupGuard>(*m_xSentenceED));
@@ -615,7 +632,7 @@ IMPL_LINK_NOARG(SpellDialog, ChangeAllHdl, weld::Button&, 
void)
     Reference<XDictionary> aXDictionary = LinguMgr::GetChangeAllList();
     DictionaryError nAdded = AddEntryToDic( aXDictionary,
             aOldWord, true,
-            aString );
+            aString, lcl_IsStripDot(aOldWord) );
 
     if(nAdded == DictionaryError::NONE)
     {
@@ -661,7 +678,7 @@ IMPL_LINK( SpellDialog, IgnoreAllHdl, weld::Button&, 
rButton, void )
         OUString sErrorText(m_xSentenceED->GetErrorText());
         DictionaryError nAdded = AddEntryToDic( aXDictionary,
             sErrorText, false,
-            OUString() );
+            OUString(), lcl_IsStripDot(sErrorText) );
         if (nAdded == DictionaryError::NONE)
         {
             std::unique_ptr<SpellUndoAction_Impl> pAction(new 
SpellUndoAction_Impl(
@@ -914,7 +931,7 @@ void SpellDialog::AddToDictionaryExecute(const OUString& 
rItemId)
     DictionaryError nAddRes = DictionaryError::UNKNOWN;
     if (xDic.is())
     {
-        nAddRes = AddEntryToDic( xDic, aNewWord, false, OUString() );
+        nAddRes = AddEntryToDic( xDic, aNewWord, false, OUString(), 
lcl_IsStripDot(aNewWord) );
         // save modified user-dictionary if it is persistent
         uno::Reference< frame::XStorable >  xSavDic( xDic, uno::UNO_QUERY );
         if (xSavDic.is())
diff --git a/linguistic/source/dicimp.cxx b/linguistic/source/dicimp.cxx
index 9c243c794933..24f6384cab3e 100644
--- a/linguistic/source/dicimp.cxx
+++ b/linguistic/source/dicimp.cxx
@@ -834,8 +834,19 @@ uno::Reference< XDictionaryEntry > SAL_CALL 
DictionaryNeo::getEntry(
     if (bNeedEntries)
         loadEntries( aMainURL );
 
+    // first search: ignore the dots at the end of the
+    // dictionary words and at the end of the query word
     sal_Int32 nPos;
-    bool bFound = seekEntry( aWord, &nPos, true );
+    bool bFound = seekEntry( aWord, &nPos,/*bSimilarOnly=*/true );
+    // don't accept an abbreviation without the dot, i.e. when the
+    // searched word is without dot, but the dictionary word is there
+    // in the dictionary only with dot
+    if ( bFound && !aWord.endsWith(".") && 
aEntries[nPos]->getDictionaryWord().endsWith(".") )
+    {
+        sal_Int32 nPos2;
+        if ( !seekEntry( aWord, &nPos2, /*bSimilarOnly=*/false ) )
+            bFound = false;
+    }
     DBG_ASSERT(!bFound || nPos < static_cast<sal_Int32>(aEntries.size()), "lng 
: index out of range");
 
     return bFound ? aEntries[ nPos ]
diff --git a/sw/source/uibase/lingu/olmenu.cxx 
b/sw/source/uibase/lingu/olmenu.cxx
index 15f49c8a9a20..85cc646ba7a2 100644
--- a/sw/source/uibase/lingu/olmenu.cxx
+++ b/sw/source/uibase/lingu/olmenu.cxx
@@ -324,6 +324,13 @@ SwSpellPopup::SwSpellPopup(
 
         m_aDics = xDicList->getDictionaries();
 
+        OUString sWord;
+        if ( m_xSpellAlt.is() )
+            sWord = m_xSpellAlt->getWord();
+        // allow to put the word with terminating dot to the custom 
dictionaries
+        // using optional menu items
+        bool bPossibleAbbreviation = sWord.endsWith(".");
+
         for (const uno::Reference<linguistic2::XDictionary>& rDic : m_aDics)
         {
             uno::Reference< linguistic2::XDictionary >  xDicTmp = rDic;
@@ -339,7 +346,9 @@ SwSpellPopup::SwSpellPopup(
             {
                 // the extra 1 is because of the (possible) external
                 // linguistic entry above
-                pMenu->InsertItem( nItemId, xDicTmp->getName() );
+                pMenu->InsertItem( nItemId, bPossibleAbbreviation
+                        ? xDicTmp->getName() + " (" + sWord.subView(0, 
sWord.getLength()-1) + ")"
+                        : xDicTmp->getName() );
                 m_aDicNameSingle = xDicTmp->getName();
 
                 if (bUseImagesInMenus)
@@ -358,6 +367,13 @@ SwSpellPopup::SwSpellPopup(
                 }
 
                 ++nItemId;
+
+                // add an optional menu item for adding the word with dot to 
this dictionary
+                if ( bPossibleAbbreviation )
+                {
+                    pMenu->InsertItem( nItemId, xDicTmp->getName() + " (" + 
sWord + ")");
+                    ++nItemId;
+                }
             }
         }
     }
diff --git a/sw/source/uibase/shells/textsh1.cxx 
b/sw/source/uibase/shells/textsh1.cxx
index 981ab67b0709..e00d90ab7be6 100644
--- a/sw/source/uibase/shells/textsh1.cxx
+++ b/sw/source/uibase/shells/textsh1.cxx
@@ -926,7 +926,7 @@ bool lcl_DeleteChartColumns(const 
uno::Reference<chart2::XChartDocument>& xChart
 }
 }
 
-static bool AddWordToWordbook(const uno::Reference<linguistic2::XDictionary>& 
xDictionary, SwWrtShell &rWrtSh)
+static bool AddWordToWordbook(const uno::Reference<linguistic2::XDictionary>& 
xDictionary, SwWrtShell &rWrtSh, bool bAddWithDot = false)
 {
     if (!xDictionary)
         return false;
@@ -937,7 +937,7 @@ static bool AddWordToWordbook(const 
uno::Reference<linguistic2::XDictionary>& xD
         return false;
 
     OUString sWord = xSpellAlt->getWord();
-    linguistic::DictionaryError nAddRes = 
linguistic::AddEntryToDic(xDictionary, sWord, false, OUString());
+    linguistic::DictionaryError nAddRes = 
linguistic::AddEntryToDic(xDictionary, sWord, false, OUString(), !bAddWithDot);
     if (linguistic::DictionaryError::NONE != nAddRes && xDictionary.is() && 
!xDictionary->getEntry(sWord).is())
     {
         SvxDicError(rWrtSh.GetView().GetFrameWeld(), nAddRes);
@@ -2363,7 +2363,7 @@ void SwTextShell::Execute(SfxRequest &rReq)
         }
         else if (sApplyText == "Spelling")
         {
-            AddWordToWordbook(LinguMgr::GetIgnoreAllList(), rWrtSh);
+            AddWordToWordbook(LinguMgr::GetIgnoreAllList(), rWrtSh );
         }
     }
     break;
@@ -2373,9 +2373,22 @@ void SwTextShell::Execute(SfxRequest &rReq)
         if (const SfxStringItem* pItem1 = 
rReq.GetArg<SfxStringItem>(FN_PARAM_1))
             aDicName = pItem1->GetValue();
 
+        // strip dot, if the extended dictionary name ends with ")", but not 
".)", e.g.
+        // "standard.dic (F.A.C.S)"  -> strip dot
+        // "standard.dic (F.A.C.S.)" -> with dot
+        bool bAddWithDot = false;
+        if ( aDicName.endsWith(")") )
+        {
+            bAddWithDot = aDicName.endsWith(".)");
+            // restore the dictionary name by stripping the parenthesized 
extension
+            sal_Int32 nHintPos = aDicName.indexOf(" (");
+            if ( nHintPos > 0 )
+                aDicName = aDicName.copy(0, nHintPos);
+        }
+
         uno::Reference<linguistic2::XSearchableDictionaryList> 
xDicList(LinguMgr::GetDictionaryList());
         uno::Reference<linguistic2::XDictionary> xDic = xDicList.is() ? 
xDicList->getDictionaryByName(aDicName) : nullptr;
-        if (AddWordToWordbook(xDic, rWrtSh))
+        if (AddWordToWordbook(xDic, rWrtSh, bAddWithDot))
         {
             // save modified user-dictionary if it is persistent
             uno::Reference<frame::XStorable> xSavDic(xDic, uno::UNO_QUERY);

Reply via email to