diff --git a/cms/conf.py b/cms/conf.py
index 5d656a2672..4d3468df77 100644
--- a/cms/conf.py
+++ b/cms/conf.py
@@ -140,12 +140,6 @@ def __init__(self):
self.max_input_length = 5_000_000 # 5 MB
self.stl_path = "/usr/share/cppreference/doc/html/"
self.docs_path = None
- # Prefix of 'shared-mime-info'[1] installation. It can be found
- # out using `pkg-config --variable=prefix shared-mime-info`, but
- # it's almost universally the same (i.e. '/usr') so it's hardly
- # necessary to change it.
- # [1] http://freedesktop.org/wiki/Software/shared-mime-info
- self.shared_mime_info_prefix = "/usr"
self.contest_admin_token = None
# AdminWebServer.
diff --git a/cms/locale/locale.py b/cms/locale/locale.py
index 951cc67716..8ce09d51ed 100644
--- a/cms/locale/locale.py
+++ b/cms/locale/locale.py
@@ -33,7 +33,6 @@
import copy
import logging
import math
-import os
import babel.core
import babel.dates
@@ -43,8 +42,8 @@
import babel.units
import importlib.resources
-from cms import config
from cmscommon.datetime import utc
+from cmscommon.mimetypes import get_name_for_type
from datetime import datetime, tzinfo, timedelta
@@ -70,9 +69,6 @@ def __init__(self, lang_code, mofile=None):
self.translation = babel.support.Translations(mofile, domain="cms")
else:
self.translation = babel.support.NullTranslations()
- self.mimetype_translation = babel.support.Translations.load(
- os.path.join(config.shared_mime_info_prefix, "share", "locale"),
- [self.locale], "shared-mime-info")
@property
def identifier(self) -> str:
@@ -266,7 +262,11 @@ def format_locale(self, code: str) -> str:
return code
def translate_mimetype(self, mimetype: str) -> str:
- return self.mimetype_translation.gettext(mimetype)
+ lang_code = self.identifier
+ alt_lang_code = babel.core.get_locale_identifier(
+ (self.locale.language, self.locale.territory), sep="_"
+ )
+ return get_name_for_type(mimetype, lang_code, alt_lang_code)
DEFAULT_TRANSLATION = Translation("en")
diff --git a/cms/server/contest/templates/task_description.html b/cms/server/contest/templates/task_description.html
index ed4c0f1d3e..b560ab9bea 100644
--- a/cms/server/contest/templates/task_description.html
+++ b/cms/server/contest/templates/task_description.html
@@ -177,10 +177,8 @@
{% trans %}Attachments{% endtrans %}
{% for filename, attachment in task.attachments|dictsort(by="key") %}
{% set mime_type = get_mimetype_for_file_name(filename) %}
{% if mime_type is not none %}
- {% set type_name = get_name_for_mimetype(mime_type) %}
{% set type_icon = get_icon_for_mimetype(mime_type) %}
{% else %}
- {% set type_name = none %}
{% set type_icon = none %}
{% endif %}
{% set file_size = handler.application.service.file_cacher.get_size(attachment.digest) %}
@@ -195,8 +193,8 @@ {% trans %}Attachments{% endtrans %}
{{ filename }}
{{ file_size|format_size }}
- {% if type_name is not none %}
- {{ translation.translate_mimetype(type_name) }}
+ {% if mime_type is not none %}
+ {{ translation.translate_mimetype(mime_type) }}
{% endif %}
diff --git a/cms/server/jinja2_toolbox.py b/cms/server/jinja2_toolbox.py
index f13cda437f..4bfd9af245 100644
--- a/cms/server/jinja2_toolbox.py
+++ b/cms/server/jinja2_toolbox.py
@@ -42,8 +42,7 @@
from cmscommon.constants import \
SCORE_MODE_MAX, SCORE_MODE_MAX_SUBTASK, SCORE_MODE_MAX_TOKENED_LAST
from cmscommon.datetime import make_datetime, make_timestamp, utc, local_tz
-from cmscommon.mimetypes import get_type_for_file_name, get_name_for_type, \
- get_icon_for_type
+from cmscommon.mimetypes import get_type_for_file_name, get_icon_for_type
@contextfilter
@@ -211,7 +210,6 @@ def instrument_cms_toolbox(env: Environment):
env.globals["get_score_type"] = safe_get_score_type
env.globals["get_mimetype_for_file_name"] = get_type_for_file_name
- env.globals["get_name_for_mimetype"] = get_name_for_type
env.globals["get_icon_for_mimetype"] = get_icon_for_type
env.filters["to_language"] = get_language
diff --git a/cmscommon/mimetypes.py b/cmscommon/mimetypes.py
index 03131c8401..9eea2cc2df 100644
--- a/cmscommon/mimetypes.py
+++ b/cmscommon/mimetypes.py
@@ -16,10 +16,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import functools
import os.path
import xdg.BaseDirectory
import xdg.Mime
+import xdg.Locale
__all__ = [
@@ -59,17 +61,36 @@ def get_icon_for_type(typename: str) -> str:
return _icons[typename]
return mimetype.media + "-x-generic"
-
-def get_name_for_type(typename: str) -> str:
+# xdg.Mime is by default memoized, but since we need to change the language, we
+# need to wipe the cache to load the correct language. So use our own caching
+# on top of it.
+@functools.cache
+def get_name_for_type(typename: str, language: str, alt_language: str) -> str:
"""Get the natural language description of the MIME type.
typename: a MIME type, e.g., "application/pdf".
+ language: the BCP47 code of the language for which to return the result.
+ alt_language: underscore-separated form of the language code, to work
+ around incorrect behavior in pyxdg.
return: the human-readable description (also called comment)
of the given MIME type, e.g., "PDF document".
"""
+ # pyxdg expects the locale field to be provided as a posix-style locale
+ # name, e.g. zh_CN. It assumes this in both the provided language name, and
+ # in the xml:lang attribute of the mimetype xml files. Some distributions
+ # instead use BCP47 language codes, e.g. zh-Hans-CN, in the mimetype xml
+ # files (which is semantically more correct, as this is mandated by the xml
+ # spec).
+ # First parse the language from the posix format.
+ xdg.Locale.update(alt_language)
+ # Then, we make pyxdg think the BCP47 code is another variant of the
+ # current language name.
+ xdg.Locale.langs += [language]
mimetype = xdg.Mime.lookup(typename).canonical()
+ # Force reloading the comment, because the language might have changed.
+ mimetype._comment = None
return mimetype.get_comment()
diff --git a/cmstestsuite/unit_tests/cmscommon/mimetypes_test.py b/cmstestsuite/unit_tests/cmscommon/mimetypes_test.py
index cd9c27a787..b2d7380dda 100755
--- a/cmstestsuite/unit_tests/cmscommon/mimetypes_test.py
+++ b/cmstestsuite/unit_tests/cmscommon/mimetypes_test.py
@@ -42,11 +42,11 @@ def test_generic(self):
class TestGetNameForType(unittest.TestCase):
def test_basic(self):
- self.assertEqual(get_name_for_type("application/pdf"),
+ self.assertEqual(get_name_for_type("application/pdf", "en", "en"),
"PDF document")
def test_alias(self):
- self.assertEqual(get_name_for_type("text/x-octave"),
+ self.assertEqual(get_name_for_type("text/x-octave", "en", "en"),
"MATLAB file")
diff --git a/cmstestsuite/unit_tests/locale/locale_test.py b/cmstestsuite/unit_tests/locale/locale_test.py
index e81d0fe458..e0c76539a2 100755
--- a/cmstestsuite/unit_tests/locale/locale_test.py
+++ b/cmstestsuite/unit_tests/locale/locale_test.py
@@ -53,6 +53,7 @@
ITALIAN = Translation("it")
DANISH = Translation("da")
CHINESE = Translation("zh_CN")
+CHINESE_TRADITIONAL = Translation("zh_TW")
class TestIdentifier(unittest.TestCase):
@@ -591,20 +592,15 @@ def test_localized_decimal_and_thousands_separators(self):
class TestTranslateMimetype(unittest.TestCase):
- @unittest.skipIf(not os.path.isfile(
- "/usr/share/locale/it/LC_MESSAGES/shared-mime-info.mo"),
- reason="need Italian shared-mime-info translation")
def test_translate_mimetype(self):
- self.assertEqual(ENGLISH.translate_mimetype("PDF document"),
+ self.assertEqual(ENGLISH.translate_mimetype("application/pdf"),
"PDF document")
- self.assertEqual(ITALIAN.translate_mimetype("PDF document"),
+ self.assertEqual(ITALIAN.translate_mimetype("application/pdf"),
"Documento PDF")
-
- def test_graceful_failure(self):
- self.assertEqual(ENGLISH.translate_mimetype("Not a MIME type"),
- "Not a MIME type")
- self.assertEqual(ITALIAN.translate_mimetype("Not a MIME type"),
- "Not a MIME type")
+ self.assertEqual(CHINESE.translate_mimetype("application/pdf"),
+ "PDF 文档")
+ self.assertEqual(CHINESE_TRADITIONAL.translate_mimetype("application/pdf"),
+ "PDF 文件")
class TestFilterLanguageCodes(unittest.TestCase):
diff --git a/docs/Installation.rst b/docs/Installation.rst
index e5eba765db..f441462e2b 100644
--- a/docs/Installation.rst
+++ b/docs/Installation.rst
@@ -70,7 +70,7 @@ On Ubuntu 24.04, one will need to run the following script as root to satisfy al
postgresql postgresql-client \
python3.12 python3.12-dev python3-pip python3-venv \
libpq-dev libcups2-dev libyaml-dev libffi-dev \
- cppreference-doc-en-html zip curl
+ shared-mime-info cppreference-doc-en-html zip curl
# Isolate from upstream package repository
echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/isolate.asc] http://www.ucw.cz/isolate/debian/ noble-isolate main' >/etc/apt/sources.list.d/isolate.list
@@ -92,7 +92,8 @@ On Arch Linux, run the following commands as root to install almost all dependen
.. sourcecode:: bash
pacman -S base-devel jdk8-openjdk fpc postgresql postgresql-client \
- python python-pip postgresql-libs libcups libyaml
+ python python-pip postgresql-libs libcups libyaml \
+ shared-mime-info
# Install the following from AUR.
# https://aur.archlinux.org/packages/cppreference/