[Fixes #13646] : Use multilang entries in ISO19115#14421
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces multilingual support for metadata fields in GeoNode by adding new template tags, utility functions for ISO 639-1/639-2 language code mapping, and updating the XML metadata templates. The reviewer identified several potential runtime errors related to input validation, data structure handling, and dictionary iteration in the new template tags and utility functions, providing specific code improvements to address these issues.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
This PR implements ISO19115 multilingual metadata rendering by adding Django template tags to surface multilang values and language descriptors, updating the default ISO19115 XML template to emit gmd:PT_FreeText/gmd:locale structures, and introducing ISO 639-1 ⇄ ISO 639-2 mapping helpers.
Changes:
- Add
multilangtemplatetags to expose multilang field checks, translations, and locale descriptors for ISO19115 templates. - Introduce ISO 639-1/639-2 mapping utilities and a default
LANGUAGE_MAPPINGSsetting. - Update the default ISO19115 template to output
gmd:LanguageCode,gmd:locale, andgmd:PT_FreeTextfortitleandabstract, plus add unit tests for the new tags.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| geonode/settings.py | Adds LANGUAGE_MAPPINGS to support ISO639-1 ⇄ ISO639-2 conversions for ISO19115 output. |
| geonode/metadata/multilang/utils.py | Adds mapping helper functions get_3_from_2() / get_2_from_3() used by the ISO template tags. |
| geonode/catalogue/templatetags/multilang.py | Introduces template tags/filters to detect multilingual fields, extract translations, and build ISO locale descriptors. |
| geonode/catalogue/templatetags/init.py | Adds the templatetags package initializer required for Django to discover the library. |
| geonode/catalogue/templates/catalogue/full_metadata.xml | Updates ISO19115 output to use gmd:LanguageCode, emit gmd:locale blocks, and render PT_FreeText for title/abstract. |
| geonode/catalogue/tests.py | Adds unit tests for the new multilang template tags. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @register.simple_tag(name="language_info") | ||
| def language_info(lang_3): | ||
| """ | ||
| Return information about a single ISO639-2 language. | ||
| """ | ||
| if not lang_3: | ||
| lang_2 = get_default_language() | ||
| lang_3 = get_3_from_2(lang_2) or "eng" | ||
| elif len(lang_3) == 2: | ||
| lang_2 = lang_3 | ||
| lang_3 = get_3_from_2(lang_2) or "eng" | ||
| else: | ||
| lang_2 = get_2_from_3(lang_3) | ||
|
|
||
| if lang_2 is None: | ||
| logger.warning( | ||
| "No ISO639-1 language found for '%s'; using the ISO639-2 code as the label.", | ||
| lang_3, | ||
| ) | ||
|
|
||
| return _language_descriptor( | ||
| lang_2, | ||
| lang_3, | ||
| ) |
| ("de", ["ger", "deu"]), | ||
| ("en", "eng"), | ||
| ("es", "spa"), | ||
| ("fr", ["fre", "fra"]), | ||
| ("it", "ita"), | ||
| ("km", "khm"), | ||
| ("nl", ["dut", "nld"]), | ||
| ("ne", "nep"), | ||
| ("fa", ["per", "fas"]), | ||
| ("pl", "pol"), | ||
| ("pt", "por"), | ||
| ("ru", "rus"), | ||
| ("si", "sin"), | ||
| ("sw", "swa"), | ||
| ("sv", "swe"), | ||
| ("tl", "tgl"), | ||
| ("ta", "tam"), | ||
| ("uk", "ukr"), | ||
| ("vi", "vie"), | ||
| ("el", ["gre", "ell"]), | ||
| ("th", "tha"), | ||
| ("zh", ["chi", "zho"]), | ||
| ("ja", "jpn"), | ||
| ("ko", "kor"), | ||
| ("sk", ["slo", "slk"]), |
| @override_settings(LANGUAGE_MAPPINGS=(("en", "eng"), ("it", "ita"))) | ||
| def test_languages_info_missing_iso_mapping_is_skipped_and_warns(self): | ||
| metadata = { | ||
| "title": "Hello", | ||
| "title_multilang_it": "Ciao", | ||
| "title_multilang_de": "Hallo", | ||
| } | ||
| result = tags.languages_info(metadata) | ||
| self.assertEqual(["locale-it"], [d["id"] for d in result]) |
| ######################################################################### | ||
| # | ||
| # Copyright (C) 2026 OSGeo | ||
| # | ||
| # This program is free software: you can redistribute it and/or modify | ||
| # it under the terms of the GNU General Public License as published by | ||
| # the Free Software Foundation, either version 3 of the License, or | ||
| # (at your option) any later version. | ||
| # | ||
| # This program is distributed in the hope that it will be useful, | ||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| # GNU General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License | ||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| # | ||
| ######################################################################### |
| Return all translations for a multilingual field except the default | ||
| language. |
There was a problem hiding this comment.
Add the default as well.
The ISO XML will match exactly the model, for instance, for title:
metadata["title"]-> element title- for each
metadata["title_multilang_xx"]-> element PT_FreeText/TextGroup
| @register.filter(name="is_multilang") | ||
| def is_multilang(field_name): | ||
| """Return whether the field is configured as multilingual.""" | ||
| return _is_multilang_field(field_name) | ||
|
|
There was a problem hiding this comment.
why do we have both
_is_multilang_field(field_name):
and
is_multilang_field(field_name):
which calls the 1-line _is_multilang_field function?
| if not isinstance(metadata, dict) or not _is_multilang_field(field_name): | ||
| return [] | ||
|
|
||
| default_language = get_default_language() | ||
| translations = [] | ||
| included_languages = set() | ||
|
|
||
| for language_code in get_2letters_languages(): | ||
| if language_code == default_language or language_code in included_languages: | ||
| continue | ||
|
|
||
| included_languages.add(language_code) | ||
|
|
||
| translated_text = _translation_value( | ||
| metadata, | ||
| field_name, | ||
| language_code, | ||
| ) | ||
|
|
||
| if translated_text: | ||
| translations.append( | ||
| { | ||
| "locale": language_code, | ||
| "text": translated_text, | ||
| } | ||
| ) | ||
|
|
There was a problem hiding this comment.
This whole stuff becomes much simpler if you use 'get_multilang_field_names(base_name)`
| LANGUAGE_MAPPINGS = ( | ||
| ("af", "afr"), | ||
| ("sq", "alb"), | ||
| ("am", "amh"), | ||
| ("ar", "ara"), | ||
| ("id", "ind"), | ||
| ("bn", "ben"), | ||
| ("de", ["ger", "deu"]), | ||
| ("en", "eng"), | ||
| ("es", "spa"), | ||
| ("fr", ["fre", "fra"]), | ||
| ("it", "ita"), | ||
| ("km", "khm"), | ||
| ("nl", ["dut", "nld"]), | ||
| ("ne", "nep"), | ||
| ("fa", ["per", "fas"]), | ||
| ("pl", "pol"), | ||
| ("pt", "por"), | ||
| ("ru", "rus"), | ||
| ("si", "sin"), | ||
| ("sw", "swa"), | ||
| ("sv", "swe"), | ||
| ("tl", "tgl"), | ||
| ("ta", "tam"), | ||
| ("uk", "ukr"), | ||
| ("vi", "vie"), | ||
| ("el", ["gre", "ell"]), | ||
| ("th", "tha"), | ||
| ("zh", ["chi", "zho"]), | ||
| ("ja", "jpn"), | ||
| ("ko", "kor"), | ||
| ("sk", ["slo", "slk"]), |
There was a problem hiding this comment.
Too many entries here, your code loops on all of them also if they are not needed.
Better to define a really small set of mappings, and let the projects add more if they need.
| for language_code in get_2letters_languages(): | ||
| if language_code == default_language or language_code in included_languages: | ||
| continue | ||
|
|
||
| included_languages.add(language_code) | ||
|
|
||
| if not any( | ||
| _has_translation(metadata, field_name, language_code) | ||
| for field_name in multilingual_fields | ||
| if _is_multilang_field(field_name) | ||
| ): | ||
| continue | ||
|
|
There was a problem hiding this comment.
Please consider using 'get_all_multilang_fields()`
| if isinstance(mappings, dict): | ||
| mappings = mappings.items() | ||
|
|
||
| for lang_2, lang_3_codes in mappings: |
There was a problem hiding this comment.
This loop will be executed over and over.
Pls consider simplifiyng it by having less entries in the mappings or caching a reversed code3->code2 mapping.
Also, we don't need a mapping for languages not set in the LANGUAGES setting.
Fixes #13646
Checklist
For all pull requests:
The following are required only for core and extension modules (they are welcomed, but not required, for contrib modules):
Submitting the PR does not require you to check all items, but by the time it gets merged, they should be either satisfied or inapplicable.