From 5982b1d337d0bbdedec675279c3f264a30f7ab26 Mon Sep 17 00:00:00 2001
From: YiFang Xiao <YiFang.xiao@foxmail.com>
Date: Wed, 4 Dec 2024 12:03:06 +0800
Subject: [PATCH 01/10] Revert "fix: <script> without src is not delayed like
 <script src="..." defer>"

This reverts commit 00dbc74bb78a199227a5dfa0a89252f9f85ea2a5.
---
 src/dict/mdx.cc           | 6 ++----
 src/scripts/gd-builtin.js | 8 --------
 2 files changed, 2 insertions(+), 12 deletions(-)

diff --git a/src/dict/mdx.cc b/src/dict/mdx.cc
index a48338e2..49a15bb1 100644
--- a/src/dict/mdx.cc
+++ b/src/dict/mdx.cc
@@ -877,8 +877,7 @@ QString & MdxDictionary::filterResource( QString & article )
 void MdxDictionary::replaceLinks( QString & id, QString & article )
 {
   QString articleNewText;
-  qsizetype linkPos = 0;
-
+  int linkPos                        = 0;
   QRegularExpressionMatchIterator it = RX::Mdx::allLinksRe.globalMatch( article );
   while ( it.hasNext() ) {
     QRegularExpressionMatch allLinksMatch = it.next();
@@ -954,8 +953,7 @@ void MdxDictionary::replaceLinks( QString & id, QString & article )
         articleNewText += linkTxt;
         match = RX::Mdx::closeScriptTagRe.match( article, linkPos );
         if ( match.hasMatch() ) {
-          articleNewText += QString( QStringLiteral( "gdOnReady(()=>{%1});</script>" ) )
-                              .arg( article.mid( linkPos, match.capturedStart() - linkPos ) );
+          articleNewText += article.mid( linkPos, match.capturedEnd() - linkPos );
           linkPos = match.capturedEnd();
         }
         continue;
diff --git a/src/scripts/gd-builtin.js b/src/scripts/gd-builtin.js
index 45c5971a..d9421c0f 100644
--- a/src/scripts/gd-builtin.js
+++ b/src/scripts/gd-builtin.js
@@ -1,11 +1,3 @@
-function gdOnReady(func) {
-  if (document.readyState !== "loading") {
-    func();
-  } else {
-    document.addEventListener("DOMContentLoaded", func);
-  }
-}
-
 function gdMakeArticleActive(newId, noEvent) {
   const gdCurrentArticle =
     document.querySelector(".gdactivearticle").attributes.id;

From 612dc8db0fff6871d95c2ff113dd344667c5dd20 Mon Sep 17 00:00:00 2001
From: YiFang Xiao <YiFang.xiao@foxmail.com>
Date: Wed, 4 Dec 2024 12:04:21 +0800
Subject: [PATCH 02/10] Revert "opt: [mdx]js script with defer attribute
 (#1869)"

This reverts commit 939e78633165c1cb1908ece5448139d8bea49b41.
---
 src/dict/mdx.cc | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/src/dict/mdx.cc b/src/dict/mdx.cc
index 49a15bb1..35c769f3 100644
--- a/src/dict/mdx.cc
+++ b/src/dict/mdx.cc
@@ -959,7 +959,7 @@ void MdxDictionary::replaceLinks( QString & id, QString & article )
         continue;
       }
       else {
-        //audio ,script,video ,html5 tags fall here.
+        //audio ,video ,html5 tags fall here.
         match = RX::Mdx::srcRe.match( linkTxt );
         if ( match.hasMatch() ) {
           QString newText;
@@ -971,15 +971,9 @@ void MdxDictionary::replaceLinks( QString & id, QString & article )
           else {
             scheme = "bres://";
           }
-
           newText =
             match.captured( 1 ) + match.captured( 2 ) + scheme + id + "/" + match.captured( 3 ) + match.captured( 2 );
 
-          //add defer to script tag
-          if ( linkType.compare( "script" ) == 0 ) {
-            newText = newText + " defer ";
-          }
-
           newLink = linkTxt.replace( match.capturedStart(), match.capturedLength(), newText );
         }
         else {

From f9d44f97c5a51e8f0d4e5d774974fea119577794 Mon Sep 17 00:00:00 2001
From: shenleban tongying <shenlebantongying@gmail.com>
Date: Sun, 8 Dec 2024 10:19:52 -0500
Subject: [PATCH 03/10] dev: force install deps in macOS workflows

---
 .github/workflows/PR-check-cmake.yml | 4 ++--
 .github/workflows/Release-all.yml    | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/PR-check-cmake.yml b/.github/workflows/PR-check-cmake.yml
index 7d55c488..5607ad6c 100644
--- a/.github/workflows/PR-check-cmake.yml
+++ b/.github/workflows/PR-check-cmake.yml
@@ -56,7 +56,7 @@ jobs:
           brew update
       - name: Install dependencies
         run: |
-          brew install \
+          brew install --force --overwrite \
           ninja \
           opencc \
           ffmpeg \
@@ -68,7 +68,7 @@ jobs:
           hunspell \
           xapian \
           libzim \
-          qt
+          qt || true
       - name: Install eb
         run: |
           wget https://github.com/mistydemeo/eb/releases/download/v4.4.3/eb-4.4.3.tar.bz2
diff --git a/.github/workflows/Release-all.yml b/.github/workflows/Release-all.yml
index b91737b7..46d8b4de 100644
--- a/.github/workflows/Release-all.yml
+++ b/.github/workflows/Release-all.yml
@@ -31,7 +31,7 @@ jobs:
           brew update
       - name: Install dependencies
         run: |
-          brew install \
+          brew install --force --overwrite \
             bzip2  \
             create-dmg \
             hunspell \
@@ -42,7 +42,7 @@ jobs:
             lzip \
             ninja \
             opencc \
-            xapian
+            xapian || true
       - name: Install eb
         run: |
           git clone https://github.com/xiaoyifang/eb.git

From a45a3092b0cf9f8703e1e93ff589b33315f1a661 Mon Sep 17 00:00:00 2001
From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com>
Date: Mon, 9 Dec 2024 09:57:21 +0800
Subject: [PATCH 04/10] opt: beautify the layout of dictserver output (#2012)

* opt: beautify the layout of dictserver output


---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
---
 src/common/utils.hh    | 13 +++++++++
 src/dict/dictserver.cc | 65 +++++++++++++++++++++++++++++++++---------
 2 files changed, 64 insertions(+), 14 deletions(-)

diff --git a/src/common/utils.hh b/src/common/utils.hh
index dfa7301e..15a4d35e 100644
--- a/src/common/utils.hh
+++ b/src/common/utils.hh
@@ -39,6 +39,19 @@ inline QString rstrip( const QString & str )
   return {};
 }
 
+inline uint32_t leadingSpaceCount( const QString & str )
+{
+  for ( int i = 0; i < str.size(); i++ ) {
+    if ( str.at( i ).isSpace() ) {
+      continue;
+    }
+    else {
+      return i;
+    }
+  }
+  return 0;
+}
+
 std::string c_string( const QString & str );
 bool endsWithIgnoreCase( QByteArrayView str, QByteArrayView extension );
 /**
diff --git a/src/dict/dictserver.cc b/src/dict/dictserver.cc
index 2dff23b2..7ca2aad5 100644
--- a/src/dict/dictserver.cc
+++ b/src/dict/dictserver.cc
@@ -592,12 +592,49 @@ public:
       cancel();
     } );
 
-    connect( this, &DictServerArticleRequest::finishedArticle, this, [ this ]( QString articleText ) {
+    connect( this, &DictServerArticleRequest::finishedArticle, this, [ this ]( QString _articleText ) {
       if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) {
         cancel();
         return;
       }
 
+      //modify the _articleText,remove extra lines[start with 15X etc.]
+      QList< QString > lines = _articleText.split( "\n", Qt::SkipEmptyParts );
+
+      QString resultStr;
+
+      // process the line
+      static QRegularExpression leadingRespCode( "^\\d{3} " );
+      uint32_t leadingSpaceCount      = 0;
+      uint32_t firstLeadingSpaceCount = 0;
+      for ( const QString & line : lines ) {
+        //ignore 15X lines
+        if ( leadingRespCode.match( line ).hasMatch() ) {
+          continue;
+        }
+        // ignore dot(.),the end line character
+        if ( line.trimmed() == "." ) {
+          break;
+        }
+
+        auto lsc = Utils::leadingSpaceCount( line );
+
+        if ( firstLeadingSpaceCount == 0 && lsc > firstLeadingSpaceCount ) {
+          firstLeadingSpaceCount = lsc;
+        }
+
+        if ( lsc >= leadingSpaceCount && lsc > firstLeadingSpaceCount ) {
+          //extra space
+          resultStr.append( " " );
+          resultStr.append( line.trimmed() );
+        }
+        else {
+          resultStr.append( "\n" );
+          resultStr.append( line );
+        }
+        leadingSpaceCount = lsc;
+      }
+
       static QRegularExpression phonetic( R"(\\([^\\]+)\\)",
                                           QRegularExpression::CaseInsensitiveOption ); // phonetics: \stuff\ ...
       static QRegularExpression divs_inside_phonetic( "</div([^>]*)><div([^>]*)>",
@@ -610,26 +647,26 @@ public:
 
       string articleStr;
       if ( contentInHtml ) {
-        articleStr = articleText.toUtf8().data();
+        articleStr = resultStr.toUtf8().data();
       }
       else {
-        articleStr = Html::preformat( articleText.toUtf8().data() );
+        articleStr = Html::preformat( resultStr.toUtf8().data() );
       }
 
-      articleText = QString::fromUtf8( articleStr.c_str(), articleStr.size() );
+      _articleText = QString::fromUtf8( articleStr.c_str(), articleStr.size() );
       int pos;
       if ( !contentInHtml ) {
-        articleText = articleText.replace( refs, R"(<a href="gdlookup://localhost/\1">\1</a>)" );
+        _articleText = _articleText.replace( refs, R"(<a href="gdlookup://localhost/\1">\1</a>)" );
 
         pos = 0;
         QString articleNewText;
 
         // Handle phonetics
 
-        QRegularExpressionMatchIterator it = phonetic.globalMatch( articleText );
+        QRegularExpressionMatchIterator it = phonetic.globalMatch( _articleText );
         while ( it.hasNext() ) {
           QRegularExpressionMatch match = it.next();
-          articleNewText += articleText.mid( pos, match.capturedStart() - pos );
+          articleNewText += _articleText.mid( pos, match.capturedStart() - pos );
           pos = match.capturedEnd();
 
           QString phonetic_text = match.captured( 1 );
@@ -638,18 +675,18 @@ public:
           articleNewText += R"(<span class="dictd_phonetic">)" + phonetic_text + "</span>";
         }
         if ( pos ) {
-          articleNewText += articleText.mid( pos );
-          articleText = articleNewText;
+          articleNewText += _articleText.mid( pos );
+          _articleText = articleNewText;
           articleNewText.clear();
         }
 
         // Handle links
 
         pos = 0;
-        it  = links.globalMatch( articleText );
+        it  = links.globalMatch( _articleText );
         while ( it.hasNext() ) {
           QRegularExpressionMatch match = it.next();
-          articleNewText += articleText.mid( pos, match.capturedStart() - pos );
+          articleNewText += _articleText.mid( pos, match.capturedStart() - pos );
           pos = match.capturedEnd();
 
           QString link = match.captured( 1 );
@@ -663,13 +700,13 @@ public:
           articleNewText += newLink;
         }
         if ( pos ) {
-          articleNewText += articleText.mid( pos );
-          articleText = articleNewText;
+          articleNewText += _articleText.mid( pos );
+          _articleText = articleNewText;
           articleNewText.clear();
         }
       }
 
-      articleData += string( "<div class=\"dictd_article\">" ) + articleText.toUtf8().data() + "<br></div>";
+      articleData += string( "<div class=\"dictd_article\">" ) + _articleText.toUtf8().data() + "<br></div>";
 
 
       if ( !articleData.empty() ) {

From a5337770da7573e4f1c2549dda10b0bdcaebbcbb Mon Sep 17 00:00:00 2001
From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com>
Date: Thu, 12 Dec 2024 15:09:44 +0800
Subject: [PATCH 05/10] fix: cjk expand character can not be searched (#2016)

* fix: cjk expand character can not be searched

* Update src/common/folding.cc

* Update src/common/globalregex.hh
---
 src/common/folding.cc     | 5 ++---
 src/common/globalregex.hh | 4 ++--
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/common/folding.cc b/src/common/folding.cc
index a5bceffb..b28397b4 100644
--- a/src/common/folding.cc
+++ b/src/common/folding.cc
@@ -22,8 +22,8 @@ std::u32string apply( std::u32string const & in, bool preserveWildcards )
 {
   // remove diacritics (normalization), white space, punt,
   auto temp = QString::fromStdU32String( in )
-                .normalized( QString::NormalizationForm_KD )
                 .remove( RX::markSpace )
+                .normalized( QString::NormalizationForm_KD )
                 .removeIf( [ preserveWildcards ]( const QChar & ch ) -> bool {
                   return ch.isPunct()
                     && !( preserveWildcards && ( ch == '\\' || ch == '?' || ch == '*' || ch == '[' || ch == ']' ) );
@@ -155,8 +155,7 @@ std::u32string applyWhitespaceAndPunctOnly( std::u32string const & in )
 
 bool isWhitespace( char32_t ch )
 {
-  //invisible character should be treated as whitespace as well.
-  return QChar::isSpace( ch ) || !QChar::isPrint( ch );
+  return QChar::isSpace( ch );
 }
 
 bool isWhitespaceOrPunct( char32_t ch )
diff --git a/src/common/globalregex.hh b/src/common/globalregex.hh
index 85fbc460..c9068960 100644
--- a/src/common/globalregex.hh
+++ b/src/common/globalregex.hh
@@ -71,8 +71,8 @@ const static QRegularExpression accentMark( R"(\p{M})", QRegularExpression::UseU
 //contain unicode space mark,invisible, and punctuation
 const static QRegularExpression markPuncSpace( R"([\p{M}\p{Z}\p{C}\p{P}])",
                                                QRegularExpression::UseUnicodePropertiesOption );
-//contain unicode space and mark.invisible
-const static QRegularExpression markSpace( R"([\p{M}\p{Z}\p{C}])", QRegularExpression::UseUnicodePropertiesOption );
+//contain unicode space and mark.
+const static QRegularExpression markSpace( R"([\p{M}\p{Z}])", QRegularExpression::UseUnicodePropertiesOption );
 
 const static QRegularExpression whiteSpace( "\\s+" );
 

From 6cd878fdcb200e069021bc44871c3c970d00dd07 Mon Sep 17 00:00:00 2001
From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com>
Date: Fri, 13 Dec 2024 10:44:54 +0800
Subject: [PATCH 06/10] opt: reorder the action (#2018)

---
 src/common/folding.cc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/folding.cc b/src/common/folding.cc
index b28397b4..f8b99d9d 100644
--- a/src/common/folding.cc
+++ b/src/common/folding.cc
@@ -22,8 +22,8 @@ std::u32string apply( std::u32string const & in, bool preserveWildcards )
 {
   // remove diacritics (normalization), white space, punt,
   auto temp = QString::fromStdU32String( in )
-                .remove( RX::markSpace )
                 .normalized( QString::NormalizationForm_KD )
+                .remove( RX::markSpace )
                 .removeIf( [ preserveWildcards ]( const QChar & ch ) -> bool {
                   return ch.isPunct()
                     && !( preserveWildcards && ( ch == '\\' || ch == '?' || ch == '*' || ch == '[' || ch == ']' ) );

From 13a50dbb6c21593ee2be11ba42660793ed908b3b Mon Sep 17 00:00:00 2001
From: shenleban tongying <shenlebantongying@gmail.com>
Date: Thu, 12 Dec 2024 23:20:39 -0500
Subject: [PATCH 07/10] fix: regression in finding {id}-{id} lang pair from
 dict name

---
 src/langcoder.cc              | 10 +++++++---
 website/docs/manage_groups.md |  4 ++--
 2 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/src/langcoder.cc b/src/langcoder.cc
index 95c33581..da22abe1 100644
--- a/src/langcoder.cc
+++ b/src/langcoder.cc
@@ -275,14 +275,18 @@ quint32 LangCoder::guessId( const QString & lang )
 
 std::pair< quint32, quint32 > LangCoder::findLangIdPairFromName( QString const & name )
 {
-  static QRegularExpression reg( "(?=([a-z]{2,3})-([a-z]{2,3}))", QRegularExpression::CaseInsensitiveOption );
+  static QRegularExpression reg( "(^|[^a-z])((?<lang1>[a-z]{2,3})-(?<lang2>[a-z]{2,3}))($|[^a-z])",
+                                 QRegularExpression::CaseInsensitiveOption );
 
   auto matches = reg.globalMatch( name );
   while ( matches.hasNext() ) {
     auto m = matches.next();
+    if ( matches.hasNext() ) {
+      continue; // We use only the last match, skip previous ones
+    }
 
-    auto fromId = guessId( m.captured( 1 ).toLower() );
-    auto toId   = guessId( m.captured( 2 ).toLower() );
+    auto fromId = guessId( m.captured( "lang1" ).toLower() );
+    auto toId   = guessId( m.captured( "lang2" ).toLower() );
 
     if ( code2Exists( intToCode2( fromId ) ) && code2Exists( intToCode2( toId ) ) ) {
       return { fromId, toId };
diff --git a/website/docs/manage_groups.md b/website/docs/manage_groups.md
index 623af88f..e5c89ec2 100644
--- a/website/docs/manage_groups.md
+++ b/website/docs/manage_groups.md
@@ -10,9 +10,9 @@ Additionally, multiple strategies of automatic grouping are provided:
 
 ## Auto groups by dictionary language
 
-For formats like DSL, which has embedded language from / to metadata, GoldenDict will use the dictionary's built-in metadata.
+For formats like DSL, which has embedded language from / to metadata, GD will use the dictionary's built-in metadata.
 
-For other formats, GoldenDict will try to extract languages from the dictionary's name or its file name by finding `{id}-{id}` pair. The `{id}` is 2 or 3 letters ISO 639 codes. For example, if a dictionary named `some name en-zh`, it will be automatically grouped into `en-zh`.
+For other formats, GD will try finding the last `{id}-{id}` pair delimited by non-alphabets in dictionary name or main file name to extract languages. The `{id}` is 2 or 3 letters ISO 639 codes. For example, if a dictionary named `some name en-zh`, it will be automatically grouped into `en-zh`.
 
 Groups created in this method also include a context menu when right-click the group name, in which you can do additional dictionaries grouping by source or target language and combine dictionaries in more large groups.
 

From e86e99e72ce4e2a3ab0efc7c7887ce7bb1f37e52 Mon Sep 17 00:00:00 2001
From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com>
Date: Mon, 16 Dec 2024 17:59:19 +0800
Subject: [PATCH 08/10] opt: combine the file filters as one entry (#2022)

* opt: combine the file filters as one entry

* Update src/ui/mainwindow.cc

Co-authored-by: shenleban tongying <shenlebantongying@gmail.com>

---------

Co-authored-by: shenleban tongying <shenlebantongying@gmail.com>
---
 src/ui/mainwindow.cc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/ui/mainwindow.cc b/src/ui/mainwindow.cc
index f614cbee..1f8f7484 100644
--- a/src/ui/mainwindow.cc
+++ b/src/ui/mainwindow.cc
@@ -4025,7 +4025,7 @@ void MainWindow::on_importFavorites_triggered()
   QString fileName = QFileDialog::getOpenFileName( this,
                                                    tr( "Import Favorites from file" ),
                                                    importPath,
-                                                   tr( "XML files (*.xml);;Txt files (*.txt);;All files (*.*)" ) );
+                                                   tr( "Text and XML files (*.txt *.xml);;All files (*.*)" ) );
   if ( fileName.size() == 0 ) {
     return;
   }

From 3cc379e2d57af559fa8e7a460914a1f8b2e0bc8c Mon Sep 17 00:00:00 2001
From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com>
Date: Tue, 17 Dec 2024 14:31:30 +0800
Subject: [PATCH 09/10] opt: replace the stardict icon (#2023)

---
 icons/icon32_stardict.png | Bin 2371 -> 8253 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)

diff --git a/icons/icon32_stardict.png b/icons/icon32_stardict.png
index 29fcffb55937c74a8834df5b9420a075853395ab..c46032fd683c24da57aae1aef6782cf17c81d9f2 100644
GIT binary patch
literal 8253
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
U+TBL*e}CYbsyZrlO4i~32Z^?-fdBvi

literal 2371
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


From b7a27d724dc13406c65ae42c4ea726f4264964de Mon Sep 17 00:00:00 2001
From: xiaoyifang <105986+xiaoyifang@users.noreply.github.com>
Date: Tue, 17 Dec 2024 20:30:04 +0800
Subject: [PATCH 10/10] opt: generate dictionary icon from dictionary name
 (#2025)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: shenleban tongying <shenlebantongying@gmail.com>
---
 src/common/dictionary_icon_name.cc | 32 ++++++++++++++
 src/common/dictionary_icon_name.hh | 24 ++++++++++
 src/common/globalbroadcaster.cc    | 18 ++++++++
 src/common/globalbroadcaster.hh    |  3 ++
 src/dict/dictionary.cc             | 71 +++++++++++++++++-------------
 src/dict/dictionary.hh             |  6 +--
 6 files changed, 120 insertions(+), 34 deletions(-)
 create mode 100644 src/common/dictionary_icon_name.cc
 create mode 100644 src/common/dictionary_icon_name.hh

diff --git a/src/common/dictionary_icon_name.cc b/src/common/dictionary_icon_name.cc
new file mode 100644
index 00000000..add0fc7c
--- /dev/null
+++ b/src/common/dictionary_icon_name.cc
@@ -0,0 +1,32 @@
+#include "dictionary_icon_name.hh"
+#include <QMutexLocker>
+
+
+QString Icons::DictionaryIconName::getIconName( const QString & dictionaryName )
+{
+  if ( dictionaryName.isEmpty() ) {
+    return {};
+  }
+  QMutexLocker _( &_mutex );
+
+  auto it = _dictionaryIconNames.contains( dictionaryName );
+  if ( it ) {
+    return _dictionaryIconNames.value( dictionaryName );
+  }
+  //get the first character of the dictionary name
+  QString name                  = dictionaryName.at( 0 ).toUpper();
+  auto it1                      = _iconDictionaryNames.contains( name );
+  std::vector< QString > vector = {};
+  if ( it1 ) {
+    vector = _iconDictionaryNames.value( name );
+    vector.emplace_back( dictionaryName );
+  }
+  else {
+    vector.emplace_back( dictionaryName );
+    _iconDictionaryNames.insert( name, vector );
+  }
+
+  name = name + QString::number( vector.size() );
+  _dictionaryIconNames.insert( dictionaryName, name );
+  return name;
+}
\ No newline at end of file
diff --git a/src/common/dictionary_icon_name.hh b/src/common/dictionary_icon_name.hh
new file mode 100644
index 00000000..6906044b
--- /dev/null
+++ b/src/common/dictionary_icon_name.hh
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <string>
+#include <QMap>
+#include <vector>
+#include <mutex>
+#include <QString>
+#include <QMutex>
+
+namespace Icons {
+//use dictionary name's (first character + the order number) to represent the dictionary name in the icon image.
+class DictionaryIconName
+{
+  //map icon name to dictionary names;
+  QMap< QString, std::vector< QString > > _iconDictionaryNames;
+  //map dictionary name to icon name;
+  QMap< QString, QString > _dictionaryIconNames;
+
+  QMutex _mutex;
+
+public:
+  QString getIconName( const QString & dictionaryName );
+};
+} // namespace Icons
\ No newline at end of file
diff --git a/src/common/globalbroadcaster.cc b/src/common/globalbroadcaster.cc
index 5fb63979..c5accf79 100644
--- a/src/common/globalbroadcaster.cc
+++ b/src/common/globalbroadcaster.cc
@@ -38,4 +38,22 @@ bool GlobalBroadcaster::existedInWhitelist( QString url ) const
 {
   return whitelist.contains( url );
 }
+
+
+QString GlobalBroadcaster::getAbbrName( QString const & text )
+{
+  if ( text.isEmpty() ) {
+    return {};
+  }
+  //remove whitespace,number,mark,puncuation,symbol
+  QString simplified = text;
+  simplified.remove(
+    QRegularExpression( R"([\p{Z}\p{N}\p{M}\p{P}\p{S}])", QRegularExpression::UseUnicodePropertiesOption ) );
+
+  if ( simplified.isEmpty() ) {
+    return {};
+  }
+
+  return _icon_names.getIconName( simplified );
+}
 // namespace global
diff --git a/src/common/globalbroadcaster.hh b/src/common/globalbroadcaster.hh
index c1d985d0..0be64a5c 100644
--- a/src/common/globalbroadcaster.hh
+++ b/src/common/globalbroadcaster.hh
@@ -5,6 +5,7 @@
 #include "config.hh"
 #include "pronounceengine.hh"
 #include <QCache>
+#include "dictionary_icon_name.hh"
 
 struct ActiveDictIds
 {
@@ -25,6 +26,7 @@ class GlobalBroadcaster: public QObject
 
   Config::Preferences * preference;
   QSet< QString > whitelist;
+  Icons::DictionaryIconName _icon_names;
 
 public:
   void setPreference( Config::Preferences * _pre );
@@ -40,6 +42,7 @@ public:
   QMap< QString, QSet< QString > > folderFavoritesMap;
   QMap< unsigned, QString > groupFolderMap;
   PronounceEngine pronounce_engine;
+  QString getAbbrName( QString const & text );
 signals:
   void dictionaryChanges( ActiveDictIds ad );
   void dictionaryClear( ActiveDictIds ad );
diff --git a/src/dict/dictionary.cc b/src/dict/dictionary.cc
index 1181b7f7..d05df52c 100644
--- a/src/dict/dictionary.cc
+++ b/src/dict/dictionary.cc
@@ -19,6 +19,7 @@
 #include <QRegularExpression>
 #include "utils.hh"
 #include "zipfile.hh"
+#include <array>
 
 namespace Dictionary {
 
@@ -291,7 +292,7 @@ bool Class::loadIconFromFile( QString const & _filename, bool isFullName )
   return false;
 }
 
-bool Class::loadIconFromText( QString iconUrl, QString const & text )
+bool Class::loadIconFromText( const QString & iconUrl, QString const & text )
 {
   if ( text.isEmpty() ) {
     return false;
@@ -308,7 +309,7 @@ bool Class::loadIconFromText( QString iconUrl, QString const & text )
     painter.setCompositionMode( QPainter::CompositionMode_SourceAtop );
 
     QFont font = painter.font();
-    //the text should be a little smaller than the icon
+    //the orderNum should be a little smaller than the icon
     font.setPixelSize( iconSize * 0.6 );
     font.setWeight( QFont::Bold );
     painter.setFont( font );
@@ -318,8 +319,21 @@ bool Class::loadIconFromText( QString iconUrl, QString const & text )
     //select a single char.
     auto abbrName = getAbbrName( text );
 
-    painter.setPen( QColor( 4, 57, 108, 200 ) );
-    painter.drawText( rectangle, Qt::AlignCenter, abbrName );
+    painter.setPen( intToFixedColor( qHash( abbrName ) ) );
+
+    // Draw first character
+    painter.drawText( rectangle, Qt::AlignCenter, abbrName.at( 0 ) );
+
+    //the orderNum should be a little smaller than the icon
+    font.setPixelSize( iconSize * 0.4 );
+    QFontMetrics fm1( font );
+    const QString & orderNum = abbrName.mid( 1 );
+    int orderNumberWidth     = fm1.horizontalAdvance( orderNum );
+
+    painter.setFont( font );
+    painter.drawText( rectangle.x() + rectangle.width() - orderNumberWidth * 1.2,
+                      rectangle.y() + rectangle.height(),
+                      orderNum );
 
     painter.end();
 
@@ -330,35 +344,30 @@ bool Class::loadIconFromText( QString iconUrl, QString const & text )
   return false;
 }
 
+QColor Class::intToFixedColor( int index )
+{
+  // Predefined list of colors
+  static const std::array colors = {
+    QColor( 255, 0, 0, 200 ),     // Red
+    QColor( 4, 57, 108, 200 ),    //Custom
+    QColor( 0, 255, 0, 200 ),     // Green
+    QColor( 0, 0, 255, 200 ),     // Blue
+    QColor( 255, 255, 0, 200 ),   // Yellow
+    QColor( 0, 255, 255, 200 ),   // Cyan
+    QColor( 255, 0, 255, 200 ),   // Magenta
+    QColor( 192, 192, 192, 200 ), // Gray
+    QColor( 255, 165, 0, 200 ),   // Orange
+    QColor( 128, 0, 128, 200 ),   // Violet
+    QColor( 128, 128, 0, 200 )    // Olive
+  };
+
+  // Use modulo operation to ensure index is within the range of the color list
+  return colors[ index % colors.size() ];
+}
+
 QString Class::getAbbrName( QString const & text )
 {
-  if ( text.isEmpty() ) {
-    return {};
-  }
-  //remove whitespace,number,mark,puncuation,symbol
-  QString simplified = text;
-  simplified.remove(
-    QRegularExpression( R"([\p{Z}\p{N}\p{M}\p{P}\p{S}])", QRegularExpression::UseUnicodePropertiesOption ) );
-
-  if ( simplified.isEmpty() ) {
-    return {};
-  }
-  int index = qHash( simplified ) % simplified.size();
-
-  QString abbrName;
-  if ( !Utils::isCJKChar( simplified.at( index ).unicode() ) ) {
-    // take two chars.
-    abbrName = simplified.mid( index, 2 );
-    if ( abbrName.size() == 1 ) {
-      //make up two characters.
-      abbrName = abbrName + simplified.at( 0 );
-    }
-  }
-  else {
-    abbrName = simplified.mid( index, 1 );
-  }
-
-  return abbrName;
+  return GlobalBroadcaster::instance()->getAbbrName( text );
 }
 
 void Class::isolateCSS( QString & css, QString const & wrapperSelector )
diff --git a/src/dict/dictionary.hh b/src/dict/dictionary.hh
index e290162d..71e13eb1 100644
--- a/src/dict/dictionary.hh
+++ b/src/dict/dictionary.hh
@@ -318,10 +318,10 @@ protected:
   // Load icon from filename directly if isFullName == true
   // else treat filename as name without extension
   bool loadIconFromFile( QString const & filename, bool isFullName = false );
-  bool loadIconFromText( QString iconUrl, QString const & text );
-
-  QString getAbbrName( QString const & text );
+  bool loadIconFromText( const QString & iconUrl, QString const & text );
 
+  static QString getAbbrName( QString const & text );
+  static QColor intToFixedColor( int index );
   /// Make css content usable only for articles from this dictionary
   void isolateCSS( QString & css, QString const & wrapperSelector = QString() );