We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Glossary check not working on spaceless languages
### Describe the issue

When having `check-glossary` enabled, the check always fails on spaceless languages, if the source string has a word in it that is in the glossary.

### I already tried

- [X] I've read and searched [the documentation](https://docs.weblate.org/).
- [X] I've searched for similar filed issues in this repository.

### Steps to reproduce the behavior

1. Create glossary entry that actually exists in a source string
2. Translate the source string with the glossary entry to Japanese (or Korean or similar)
3. Enable `check-glossary`
4. Notice a false positive on the check

### Expected behavior

_No response_

### Screenshots

![Bildschirmfoto vom 2024-03-30 17-12-24](https://github.com/WeblateOrg/weblate/assets/11144627/d906ae16-7c70-42f7-943a-ac9f03b0517d)
![Bildschirmfoto vom 2024-03-30 17-12-20](https://github.com/WeblateOrg/weblate/assets/11144627/65995849-2de4-4df8-a05c-230964d257cc)


### Exception traceback

_No response_

### How do you run Weblate?

Docker container

### Weblate versions

 * Weblate: 5.4.3
 * Django: 4.2.11
 * siphashc: 2.4.1
 * translate-toolkit: 3.12.2
 * lxml: 5.1.0
 * pillow: 10.2.0
 * nh3: 0.2.17
 * python-dateutil: 2.9.0.post0
 * social-auth-core: 4.5.3
 * social-auth-app-django: 5.4.0
 * django-crispy-forms: 2.1
 * oauthlib: 3.2.2
 * django-compressor: 4.4
 * djangorestframework: 3.14.0
 * django-filter: 23.5
 * django-appconf: 1.0.6
 * user-agents: 2.2.0
 * filelock: 3.13.3
 * rapidfuzz: 3.7.0
 * openpyxl: 3.1.2
 * celery: 5.3.6
 * django-celery-beat: 2.5.0
 * kombu: 5.3.5
 * translation-finder: 2.16
 * weblate-language-data: 2024.3
 * html2text: 2020.1.16
 * pycairo: 1.26.0
 * PyGObject: 3.48.1
 * diff-match-patch: 20230430
 * requests: 2.31.0
 * django-redis: 5.4.0
 * hiredis: 2.3.2
 * sentry-sdk: 1.43.0
 * Cython: 3.0.9
 * misaka: 2.1.1
 * GitPython: 3.1.42
 * borgbackup: 1.2.7
 * pyparsing: 3.1.2
 * ahocorasick_rs: 0.22.0
 * python-redis-lock: 4.0.0
 * charset-normalizer: 3.3.2
 * Python: 3.12.2
 * Git: 2.39.2
 * psycopg: 3.1.18
 * psycopg-binary: 3.1.18
 * phply: 1.2.6
 * ruamel.yaml: 0.18.6
 * tesserocr: 2.6.2
 * boto3: 1.34.71
 * zeep: 4.2.1
 * aeidon: 1.13
 * iniparse: 0.5
 * mysqlclient: 2.2.4
 * Mercurial: 6.7.1
 * git-svn: 2.39.2
 * git-review: 2.4.0
 * PostgreSQL server: 15.2
 * Database backends: django.db.backends.postgresql
 * PostgreSQL implementation: psycopg3 (binary)
 * Cache backends: default:RedisCache, avatar:FileBasedCache
 * Email setup: django.core.mail.backends.smtp.EmailBackend: mail.your-server.de
 * OS encoding: filesystem=utf-8, default=utf-8
 * Celery: redis://cache:6379/1, redis://cache:6379/1, regular
 * Platform: Linux 5.10.0-28-amd64 (x86_64)

### Weblate deploy checks

```shell
System check identified some issues:

INFOS:
?: (weblate.I028) Backups are not configured, it is highly recommended for production use
	HINT: https://docs.weblate.org/en/weblate-5.4.3/admin/backup.html

System check identified 1 issue (1 silenced).
```


### Additional context

_No response_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `weblate/checks/glossary.py`
Content:
```
1 # Copyright © Michal Čihař <michal@weblate.org>
2 #
3 # SPDX-License-Identifier: GPL-3.0-or-later
4 
5 import re
6 
7 from django.utils.html import escape, format_html, format_html_join
8 from django.utils.translation import gettext, gettext_lazy
9 
10 from weblate.checks.base import TargetCheck
11 
12 
13 class GlossaryCheck(TargetCheck):
14     default_disabled = True
15     check_id = "check_glossary"
16     name = gettext_lazy("Does not follow glossary")
17     description = gettext_lazy(
18         "The translation does not follow terms defined in a glossary."
19     )
20 
21     def check_single(self, source, target, unit):
22         from weblate.glossary.models import get_glossary_terms
23 
24         forbidden = set()
25         mismatched = set()
26         matched = set()
27         for term in get_glossary_terms(unit):
28             term_source = term.source
29             flags = term.all_flags
30             expected = term_source if "read-only" in flags else term.target
31             if "forbidden" in flags:
32                 if re.search(rf"\b{re.escape(expected)}\b", target, re.IGNORECASE):
33                     forbidden.add(term_source)
34             else:
35                 if term_source in matched:
36                     continue
37                 if re.search(rf"\b{re.escape(expected)}\b", target, re.IGNORECASE):
38                     mismatched.discard(term_source)
39                     matched.add(term_source)
40                 else:
41                     mismatched.add(term_source)
42 
43         return forbidden | mismatched
44 
45     def get_description(self, check_obj):
46         unit = check_obj.unit
47         sources = unit.get_source_plurals()
48         targets = unit.get_target_plurals()
49         source = sources[0]
50         results = set()
51         # Check singular
52         result = self.check_single(source, targets[0], unit)
53         if result:
54             results.update(result)
55         # Do we have more to check?
56         if len(sources) > 1:
57             source = sources[1]
58         # Check plurals against plural from source
59         for target in targets[1:]:
60             result = self.check_single(source, target, unit)
61             if result:
62                 results.update(result)
63 
64         if not results:
65             return super().get_description(check_obj)
66 
67         return format_html(
68             escape(
69                 gettext("Following terms are not translated according to glossary: {}")
70             ),
71             format_html_join(", ", "{}", ((term,) for term in sorted(results))),
72         )
73
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/weblate/checks/glossary.py b/weblate/checks/glossary.py
--- a/weblate/checks/glossary.py
+++ b/weblate/checks/glossary.py
@@ -24,17 +24,22 @@
         forbidden = set()
         mismatched = set()
         matched = set()
+        boundary = r"\b" if unit.translation.language.uses_whitespace() else ""
         for term in get_glossary_terms(unit):
             term_source = term.source
             flags = term.all_flags
             expected = term_source if "read-only" in flags else term.target
             if "forbidden" in flags:
-                if re.search(rf"\b{re.escape(expected)}\b", target, re.IGNORECASE):
+                if re.search(
+                    rf"{boundary}{re.escape(expected)}{boundary}", target, re.IGNORECASE
+                ):
                     forbidden.add(term_source)
             else:
                 if term_source in matched:
                     continue
-                if re.search(rf"\b{re.escape(expected)}\b", target, re.IGNORECASE):
+                if re.search(
+                    rf"{boundary}{re.escape(expected)}{boundary}", target, re.IGNORECASE
+                ):
                     mismatched.discard(term_source)
                     matched.add(term_source)
                 else:

{"golden_diff": "diff --git a/weblate/checks/glossary.py b/weblate/checks/glossary.py\n--- a/weblate/checks/glossary.py\n+++ b/weblate/checks/glossary.py\n@@ -24,17 +24,22 @@\n         forbidden = set()\n         mismatched = set()\n         matched = set()\n+        boundary = r\"\\b\" if unit.translation.language.uses_whitespace() else \"\"\n         for term in get_glossary_terms(unit):\n             term_source = term.source\n             flags = term.all_flags\n             expected = term_source if \"read-only\" in flags else term.target\n             if \"forbidden\" in flags:\n-                if re.search(rf\"\\b{re.escape(expected)}\\b\", target, re.IGNORECASE):\n+                if re.search(\n+                    rf\"{boundary}{re.escape(expected)}{boundary}\", target, re.IGNORECASE\n+                ):\n                     forbidden.add(term_source)\n             else:\n                 if term_source in matched:\n                     continue\n-                if re.search(rf\"\\b{re.escape(expected)}\\b\", target, re.IGNORECASE):\n+                if re.search(\n+                    rf\"{boundary}{re.escape(expected)}{boundary}\", target, re.IGNORECASE\n+                ):\n                     mismatched.discard(term_source)\n                     matched.add(term_source)\n                 else:\n", "issue": "Glossary check not working on spaceless languages\n### Describe the issue\n\nWhen having `check-glossary` enabled, the check always fails on spaceless languages, if the source string has a word in it that is in the glossary.\n\n### I already tried\n\n- [X] I've read and searched [the documentation](https://docs.weblate.org/).\n- [X] I've searched for similar filed issues in this repository.\n\n### Steps to reproduce the behavior\n\n1. Create glossary entry that actually exists in a source string\r\n2. Translate the source string with the glossary entry to Japanese (or Korean or similar)\r\n3. Enable `check-glossary`\r\n4. Notice a false positive on the check\n\n### Expected behavior\n\n_No response_\n\n### Screenshots\n\n![Bildschirmfoto vom 2024-03-30 17-12-24](https://github.com/WeblateOrg/weblate/assets/11144627/d906ae16-7c70-42f7-943a-ac9f03b0517d)\r\n![Bildschirmfoto vom 2024-03-30 17-12-20](https://github.com/WeblateOrg/weblate/assets/11144627/65995849-2de4-4df8-a05c-230964d257cc)\r\n\n\n### Exception traceback\n\n_No response_\n\n### How do you run Weblate?\n\nDocker container\n\n### Weblate versions\n\n * Weblate: 5.4.3\r\n * Django: 4.2.11\r\n * siphashc: 2.4.1\r\n * translate-toolkit: 3.12.2\r\n * lxml: 5.1.0\r\n * pillow: 10.2.0\r\n * nh3: 0.2.17\r\n * python-dateutil: 2.9.0.post0\r\n * social-auth-core: 4.5.3\r\n * social-auth-app-django: 5.4.0\r\n * django-crispy-forms: 2.1\r\n * oauthlib: 3.2.2\r\n * django-compressor: 4.4\r\n * djangorestframework: 3.14.0\r\n * django-filter: 23.5\r\n * django-appconf: 1.0.6\r\n * user-agents: 2.2.0\r\n * filelock: 3.13.3\r\n * rapidfuzz: 3.7.0\r\n * openpyxl: 3.1.2\r\n * celery: 5.3.6\r\n * django-celery-beat: 2.5.0\r\n * kombu: 5.3.5\r\n * translation-finder: 2.16\r\n * weblate-language-data: 2024.3\r\n * html2text: 2020.1.16\r\n * pycairo: 1.26.0\r\n * PyGObject: 3.48.1\r\n * diff-match-patch: 20230430\r\n * requests: 2.31.0\r\n * django-redis: 5.4.0\r\n * hiredis: 2.3.2\r\n * sentry-sdk: 1.43.0\r\n * Cython: 3.0.9\r\n * misaka: 2.1.1\r\n * GitPython: 3.1.42\r\n * borgbackup: 1.2.7\r\n * pyparsing: 3.1.2\r\n * ahocorasick_rs: 0.22.0\r\n * python-redis-lock: 4.0.0\r\n * charset-normalizer: 3.3.2\r\n * Python: 3.12.2\r\n * Git: 2.39.2\r\n * psycopg: 3.1.18\r\n * psycopg-binary: 3.1.18\r\n * phply: 1.2.6\r\n * ruamel.yaml: 0.18.6\r\n * tesserocr: 2.6.2\r\n * boto3: 1.34.71\r\n * zeep: 4.2.1\r\n * aeidon: 1.13\r\n * iniparse: 0.5\r\n * mysqlclient: 2.2.4\r\n * Mercurial: 6.7.1\r\n * git-svn: 2.39.2\r\n * git-review: 2.4.0\r\n * PostgreSQL server: 15.2\r\n * Database backends: django.db.backends.postgresql\r\n * PostgreSQL implementation: psycopg3 (binary)\r\n * Cache backends: default:RedisCache, avatar:FileBasedCache\r\n * Email setup: django.core.mail.backends.smtp.EmailBackend: mail.your-server.de\r\n * OS encoding: filesystem=utf-8, default=utf-8\r\n * Celery: redis://cache:6379/1, redis://cache:6379/1, regular\r\n * Platform: Linux 5.10.0-28-amd64 (x86_64)\n\n### Weblate deploy checks\n\n```shell\nSystem check identified some issues:\r\n\r\nINFOS:\r\n?: (weblate.I028) Backups are not configured, it is highly recommended for production use\r\n\tHINT: https://docs.weblate.org/en/weblate-5.4.3/admin/backup.html\r\n\r\nSystem check identified 1 issue (1 silenced).\n```\n\n\n### Additional context\n\n_No response_\n", "before_files": [{"content": "# Copyright \u00a9 Michal \u010ciha\u0159 <michal@weblate.org>\n#\n# SPDX-License-Identifier: GPL-3.0-or-later\n\nimport re\n\nfrom django.utils.html import escape, format_html, format_html_join\nfrom django.utils.translation import gettext, gettext_lazy\n\nfrom weblate.checks.base import TargetCheck\n\n\nclass GlossaryCheck(TargetCheck):\n    default_disabled = True\n    check_id = \"check_glossary\"\n    name = gettext_lazy(\"Does not follow glossary\")\n    description = gettext_lazy(\n        \"The translation does not follow terms defined in a glossary.\"\n    )\n\n    def check_single(self, source, target, unit):\n        from weblate.glossary.models import get_glossary_terms\n\n        forbidden = set()\n        mismatched = set()\n        matched = set()\n        for term in get_glossary_terms(unit):\n            term_source = term.source\n            flags = term.all_flags\n            expected = term_source if \"read-only\" in flags else term.target\n            if \"forbidden\" in flags:\n                if re.search(rf\"\\b{re.escape(expected)}\\b\", target, re.IGNORECASE):\n                    forbidden.add(term_source)\n            else:\n                if term_source in matched:\n                    continue\n                if re.search(rf\"\\b{re.escape(expected)}\\b\", target, re.IGNORECASE):\n                    mismatched.discard(term_source)\n                    matched.add(term_source)\n                else:\n                    mismatched.add(term_source)\n\n        return forbidden | mismatched\n\n    def get_description(self, check_obj):\n        unit = check_obj.unit\n        sources = unit.get_source_plurals()\n        targets = unit.get_target_plurals()\n        source = sources[0]\n        results = set()\n        # Check singular\n        result = self.check_single(source, targets[0], unit)\n        if result:\n            results.update(result)\n        # Do we have more to check?\n        if len(sources) > 1:\n            source = sources[1]\n        # Check plurals against plural from source\n        for target in targets[1:]:\n            result = self.check_single(source, target, unit)\n            if result:\n                results.update(result)\n\n        if not results:\n            return super().get_description(check_obj)\n\n        return format_html(\n            escape(\n                gettext(\"Following terms are not translated according to glossary: {}\")\n            ),\n            format_html_join(\", \", \"{}\", ((term,) for term in sorted(results))),\n        )\n", "path": "weblate/checks/glossary.py"}], "after_files": [{"content": "# Copyright \u00a9 Michal \u010ciha\u0159 <michal@weblate.org>\n#\n# SPDX-License-Identifier: GPL-3.0-or-later\n\nimport re\n\nfrom django.utils.html import escape, format_html, format_html_join\nfrom django.utils.translation import gettext, gettext_lazy\n\nfrom weblate.checks.base import TargetCheck\n\n\nclass GlossaryCheck(TargetCheck):\n    default_disabled = True\n    check_id = \"check_glossary\"\n    name = gettext_lazy(\"Does not follow glossary\")\n    description = gettext_lazy(\n        \"The translation does not follow terms defined in a glossary.\"\n    )\n\n    def check_single(self, source, target, unit):\n        from weblate.glossary.models import get_glossary_terms\n\n        forbidden = set()\n        mismatched = set()\n        matched = set()\n        boundary = r\"\\b\" if unit.translation.language.uses_whitespace() else \"\"\n        for term in get_glossary_terms(unit):\n            term_source = term.source\n            flags = term.all_flags\n            expected = term_source if \"read-only\" in flags else term.target\n            if \"forbidden\" in flags:\n                if re.search(\n                    rf\"{boundary}{re.escape(expected)}{boundary}\", target, re.IGNORECASE\n                ):\n                    forbidden.add(term_source)\n            else:\n                if term_source in matched:\n                    continue\n                if re.search(\n                    rf\"{boundary}{re.escape(expected)}{boundary}\", target, re.IGNORECASE\n                ):\n                    mismatched.discard(term_source)\n                    matched.add(term_source)\n                else:\n                    mismatched.add(term_source)\n\n        return forbidden | mismatched\n\n    def get_description(self, check_obj):\n        unit = check_obj.unit\n        sources = unit.get_source_plurals()\n        targets = unit.get_target_plurals()\n        source = sources[0]\n        results = set()\n        # Check singular\n        result = self.check_single(source, targets[0], unit)\n        if result:\n            results.update(result)\n        # Do we have more to check?\n        if len(sources) > 1:\n            source = sources[1]\n        # Check plurals against plural from source\n        for target in targets[1:]:\n            result = self.check_single(source, target, unit)\n            if result:\n                results.update(result)\n\n        if not results:\n            return super().get_description(check_obj)\n\n        return format_html(\n            escape(\n                gettext(\"Following terms are not translated according to glossary: {}\")\n            ),\n            format_html_join(\", \", \"{}\", ((term,) for term in sorted(results))),\n        )\n", "path": "weblate/checks/glossary.py"}]}