fix: Internacionalização de nomes de idiomas nos menus de artigos#373
fix: Internacionalização de nomes de idiomas nos menus de artigos#373Rossi-Luciano wants to merge 2 commits intoscieloorg:masterfrom
Conversation
- Adiciona dicionário LANGUAGE_TRANSLATIONS com 40+ idiomas em choices.py - Adiciona função get_language_name() para traduzir códigos ISO - Adiciona função get_language_code_upper() para códigos ISO maiúsculos - Adiciona função register_language_filters() para registrar filtros Jinja2 - Usa g.interface_language (compatível com PR scieloorg#366) - Suporta traduções de interface em pt/en/es Corrige: TK-355
- Exibe nomes de idiomas traduzidos nos menus de resumo/texto/PDF
- Mostra códigos ISO em maiúsculo no botão colapsado (DE, FR, RU)
- Mostra nomes traduzidos no dropdown expandido (Alemão, German, Alemán)
- Corrige IDs duplicados em levelMenu_pdf.html (btnGroupDropPDF)
- Usa g.interface_language ao invés de session.get('lang')
- Compatível com abordagem Accept-Language header do PR scieloorg#366
Antes: Resumo (de), Texto (fr), PDF (ru)
Depois: Resumo (Alemão), Texto (Francês), PDF (Russo)
Corrige: TK-355
There was a problem hiding this comment.
Pull request overview
This PR implements internationalization for language names in article menus (abstract, text, PDF), addressing an issue where languages beyond Portuguese, English, and Spanish appeared as raw ISO codes instead of translated names. The implementation introduces a centralized translation dictionary with 40+ languages and Jinja2 filters for automatic translation based on the interface language.
Changes:
- Added
LANGUAGE_TRANSLATIONSdictionary inchoices.pywith translations for 40+ languages across pt/en/es interfaces - Created utility functions (
get_language_name,get_language_code_upper,register_language_filters) inutils.pyfor language name resolution - Updated templates (levelMenu_abstracts.html, levelMenu_texts.html, levelMenu_pdf.html) to use new translation filters
- Fixed duplicate HTML IDs in PDF menu (btnGroupDropPDFSingle vs btnGroupDropPDFMultiple)
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| opac/webapp/choices.py | Adds LANGUAGE_TRANSLATIONS dictionary with 40+ languages, each with translations to pt/en/es |
| opac/webapp/utils/utils.py | Implements language translation utility functions and Jinja2 filter registration |
| opac/webapp/templates/article/includes/levelMenu_abstracts.html | Replaces hardcoded language name conditionals with dynamic filter-based translation |
| opac/webapp/templates/article/includes/levelMenu_texts.html | Replaces hardcoded language name conditionals with dynamic filter-based translation |
| opac/webapp/templates/article/includes/levelMenu_pdf.html | Replaces hardcoded language name conditionals with dynamic filter-based translation; fixes duplicate ID issue |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {# Dropdown expandido: mostra nome traduzido do idioma #} | ||
| {# g.interface_language contém o idioma da interface (pt/en/es) #} | ||
| {# Compatível com PR #366 - migração de session.get('lang') para g.interface_language #} | ||
| ({{ abstract['language']|language_name(g.interface_language) }}) |
There was a problem hiding this comment.
The variable g.interface_language is used but does not appear to be set anywhere in the codebase. The current implementation uses session['lang'] (as seen in webapp/main/views.py in the get_locale() function). Until PR #370 is merged and g.interface_language is properly set, this code will fail with an AttributeError. Consider using a fallback approach that works with the current codebase, such as using session.get('lang', 'en') or calling get_locale() directly.
| ({{ abstract['language'] }}) | ||
| {% endif %} | ||
| {# Dropdown expandido: mostra nome traduzido do idioma #} | ||
| ({{ abstract['language']|language_name(g.interface_language) }}) |
There was a problem hiding this comment.
The variable g.interface_language is used but does not appear to be set anywhere in the codebase. The current implementation uses session['lang'] (as seen in webapp/main/views.py in the get_locale() function). Until PR #370 is merged and g.interface_language is properly set, this code will fail with an AttributeError. Consider using a fallback approach that works with the current codebase, such as using session.get('lang', 'en') or calling get_locale() directly.
| {# Dropdown expandido: mostra nome traduzido do idioma #} | ||
| {# g.interface_language contém o idioma da interface (pt/en/es) #} | ||
| {# Compatível com PR #366 - migração de session.get('lang') para g.interface_language #} | ||
| ({{ lang|language_name(g.interface_language) }}) |
There was a problem hiding this comment.
The variable g.interface_language is used but does not appear to be set anywhere in the codebase. The current implementation uses session['lang'] (as seen in webapp/main/views.py in the get_locale() function). Until PR #370 is merged and g.interface_language is properly set, this code will fail with an AttributeError. Consider using a fallback approach that works with the current codebase, such as using session.get('lang', 'en') or calling get_locale() directly.
| {# Dropdown: mostra "Download PDF" com nome traduzido do idioma #} | ||
| {# g.interface_language contém o idioma da interface (pt/en/es) #} | ||
| {# Compatível com PR #366 - migração de session.get('lang') para g.interface_language #} | ||
| {% trans %}Download PDF{% endtrans %} ({{ pdf.lang|language_name(g.interface_language) }}) |
There was a problem hiding this comment.
The variable g.interface_language is used but does not appear to be set anywhere in the codebase. The current implementation uses session['lang'] (as seen in webapp/main/views.py in the get_locale() function). Until PR #370 is merged and g.interface_language is properly set, this code will fail with an AttributeError. Consider using a fallback approach that works with the current codebase, such as using session.get('lang', 'en') or calling get_locale() directly.
| {{ pdf.lang }} | ||
| {% endif %} | ||
| {# Dropdown: mostra "Download PDF" com nome traduzido do idioma #} | ||
| {% trans %}Download PDF{% endtrans %} ({{ pdf.lang|language_name(g.interface_language) }}) |
There was a problem hiding this comment.
The variable g.interface_language is used but does not appear to be set anywhere in the codebase. The current implementation uses session['lang'] (as seen in webapp/main/views.py in the get_locale() function). Until PR #370 is merged and g.interface_language is properly set, this code will fail with an AttributeError. Consider using a fallback approach that works with the current codebase, such as using session.get('lang', 'en') or calling get_locale() directly.
|
|
||
| # Também disponibiliza como função global nos templates | ||
| app.jinja_env.globals['get_language_name'] = get_language_name | ||
| app.jinja_env.globals['get_language_code_upper'] = get_language_code_upper |
There was a problem hiding this comment.
The function register_language_filters() is defined but never called. The Jinja2 filters language_name and language_code_upper need to be registered during application initialization (e.g., in create_app() function in webapp/__init__.py) for them to be available in templates. Without calling this function, the templates will fail with an "UndefinedError: No filter named 'language_name'" error.
| app.jinja_env.globals['get_language_code_upper'] = get_language_code_upper | |
| app.jinja_env.globals['get_language_code_upper'] = get_language_code_upper | |
| # Registra automaticamente os filtros se existir uma instância global de app | |
| if hasattr(webapp, 'app'): | |
| register_language_filters(webapp.app) |
| def get_language_name(lang_code, interface_lang='en'): | ||
| """ | ||
| Retorna o nome do idioma traduzido de acordo com o idioma da interface. | ||
|
|
||
| Args: | ||
| lang_code (str): Código ISO 639-1 do idioma (ex: 'de', 'fr', 'ru') | ||
| interface_lang (str): Idioma da interface ('pt', 'en' ou 'es') | ||
|
|
||
| Returns: | ||
| str: Nome do idioma traduzido ou código ISO em maiúsculo se não encontrado | ||
|
|
||
| Exemplos: | ||
| >>> get_language_name('de', 'pt') | ||
| 'Alemão' | ||
| >>> get_language_name('de', 'en') | ||
| 'German' | ||
| >>> get_language_name('fr', 'es') | ||
| 'Francés' | ||
| >>> get_language_name('xx', 'pt') | ||
| 'XX' | ||
| """ | ||
|
|
||
| if not lang_code: | ||
| return '' | ||
|
|
||
| lang_code = lang_code.lower() | ||
| interface_lang = interface_lang.lower() | ||
|
|
||
| # Se o código não está no dicionário, retorna em maiúsculo | ||
| if lang_code not in LANGUAGE_TRANSLATIONS: | ||
| return lang_code.upper() | ||
|
|
||
| # Se o idioma da interface não está disponível, usa inglês como fallback | ||
| if interface_lang not in LANGUAGE_TRANSLATIONS[lang_code]: | ||
| interface_lang = 'en' | ||
|
|
||
| # Retorna a tradução (lazy_gettext precisa ser convertido para string) | ||
| translation = LANGUAGE_TRANSLATIONS[lang_code].get(interface_lang, lang_code.upper()) | ||
| return str(translation) | ||
|
|
||
|
|
||
| def get_language_code_upper(lang_code): | ||
| """ | ||
| Retorna o código ISO do idioma em maiúsculo. | ||
|
|
||
| Args: | ||
| lang_code (str): Código ISO 639-1 do idioma | ||
|
|
||
| Returns: | ||
| str: Código em maiúsculo (ex: 'DE', 'FR', 'RU') | ||
|
|
||
| Exemplos: | ||
| >>> get_language_code_upper('de') | ||
| 'DE' | ||
| >>> get_language_code_upper('fr') | ||
| 'FR' | ||
| """ | ||
| return lang_code.upper() if lang_code else '' | ||
|
|
||
|
|
||
| def register_language_filters(app): | ||
| """ | ||
| Registra os filtros Jinja2 para tradução de idiomas. | ||
|
|
||
| Args: | ||
| app: Instância do Flask app | ||
|
|
||
| Uso nos templates: | ||
| {{ 'de'|language_name('pt') }} -> 'Alemão' | ||
| {{ 'de'|language_code_upper }} -> 'DE' | ||
| """ | ||
| app.jinja_env.filters['language_name'] = get_language_name | ||
| app.jinja_env.filters['language_code_upper'] = get_language_code_upper | ||
|
|
||
| # Também disponibiliza como função global nos templates | ||
| app.jinja_env.globals['get_language_name'] = get_language_name | ||
| app.jinja_env.globals['get_language_code_upper'] = get_language_code_upper |
There was a problem hiding this comment.
The new utility functions get_language_name(), get_language_code_upper(), and register_language_filters() lack test coverage. The repository has comprehensive test coverage for utils (see test_utils.py), and these new functions should have unit tests to verify: 1) correct translation for supported languages, 2) fallback behavior for unsupported languages, 3) handling of None/empty input, 4) case-insensitive language code handling, and 5) filter registration functionality.
| {% endif %} | ||
| {# Dropdown expandido: mostra nome traduzido do idioma #} | ||
| {# g.interface_language contém o idioma da interface (pt/en/es) #} | ||
| {# Compatível com PR #366 - migração de session.get('lang') para g.interface_language #} |
There was a problem hiding this comment.
The comment references "PR #366" but the PR description consistently mentions "PR #370" as the related PR for migrating from session.get('lang') to g.interface_language. This inconsistency could cause confusion for future maintainers. Please verify and correct the PR number in the comment.
| {# Compatível com PR #366 - migração de session.get('lang') para g.interface_language #} | |
| {# Compatível com PR #370 - migração de session.get('lang') para g.interface_language #} |
| {% endif %} | ||
| {# Dropdown: mostra "Download PDF" com nome traduzido do idioma #} | ||
| {# g.interface_language contém o idioma da interface (pt/en/es) #} | ||
| {# Compatível com PR #366 - migração de session.get('lang') para g.interface_language #} |
There was a problem hiding this comment.
| 'al': { # Alias (compatibilidade com ISO3166_ALPHA2 existente) | ||
| 'pt': __('Albanês'), | ||
| 'en': __('Albanian'), | ||
| 'es': __('Albanés') | ||
| }, | ||
| 'sq': { # Código ISO correto para albanês |
There was a problem hiding this comment.
The comment indicates 'al' is an "Alias (compatibilidade com ISO3166_ALPHA2 existente)", but this is misleading. 'al' is an ISO 3166 country code (Albania), not an ISO 639 language code. ISO 639-1 uses 'sq' for Albanian language. While keeping 'al' for backward compatibility may be necessary, the comment should clarify that 'al' is not a valid ISO 639 language code and exists only for legacy support.
| 'al': { # Alias (compatibilidade com ISO3166_ALPHA2 existente) | |
| 'pt': __('Albanês'), | |
| 'en': __('Albanian'), | |
| 'es': __('Albanés') | |
| }, | |
| 'sq': { # Código ISO correto para albanês | |
| 'al': { # Alias legado para compatibilidade; 'al' é código ISO 3166 (Albânia), não ISO 639-1. Use 'sq' para albanês. | |
| 'pt': __('Albanês'), | |
| 'en': __('Albanian'), | |
| 'es': __('Albanés') | |
| }, | |
| 'sq': { # Código ISO 639-1 correto para albanês |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 'al': { # Alias (compatibilidade com ISO3166_ALPHA2 existente) | ||
| 'pt': __('Albanês'), | ||
| 'en': __('Albanian'), | ||
| 'es': __('Albanés') | ||
| }, |
There was a problem hiding this comment.
The code 'al' is an ISO 3166-1 country code for Albania, not an ISO 639-1 language code. The correct language code for Albanian is 'sq', which is already defined below. While this may be added for backward compatibility as the comment indicates, having both could cause confusion. Consider documenting why 'al' is needed or removing it if it's not actively used in the database.
There was a problem hiding this comment.
@Rossi-Luciano acho que a abordagem deste dicionário não é sustentável, além disso no lugar de idioma está sendo usado país. Use a lista iso de idiomas. Crie um dicionário simples de chave e valor sendo a chave o código do idioma e o valor o nome do idioma no idioma original ou em inglês dentro da marca {% trans %}{{ lang_name }}{% endtrans %}
| ({{ abstract['language']|language_name(g.interface_language) }}) | ||
| </a> |
There was a problem hiding this comment.
The templates reference g.interface_language, but this variable is not defined anywhere in the current codebase. The templates will fail with an AttributeError when trying to access this undefined attribute.
If this PR depends on PR #370 (or #366 as mentioned in comments) that introduces g.interface_language, that dependency needs to be clearly stated and this PR should not be merged until the dependent PR is merged first. Alternatively, you need to add a fallback that uses the current session.get('lang') approach until the migration is complete.
| if not lang_code: | ||
| return '' | ||
|
|
||
| lang_code = lang_code.lower() |
There was a problem hiding this comment.
The function should handle the case where interface_lang is None more explicitly. While line 826 does .lower() on it, if interface_lang is None, this will raise an AttributeError. Consider adding a check: if not interface_lang: interface_lang = 'en' before line 826.
| lang_code = lang_code.lower() | |
| lang_code = lang_code.lower() | |
| if not interface_lang: | |
| interface_lang = 'en' |
| translation = LANGUAGE_TRANSLATIONS[lang_code].get(interface_lang, lang_code.upper()) | ||
| return str(translation) |
There was a problem hiding this comment.
The language_name filter is being called inside loops that iterate over abstracts/texts/PDFs. Each call converts the lazy_gettext result to a string (line 838 in utils.py). While this is unlikely to cause performance issues with a small number of items, consider that the lazy_gettext objects could be evaluated multiple times for the same language code. This is acceptable for the current use case but worth noting if the pattern is extended to larger datasets.
| # Se o idioma da interface não está disponível, usa inglês como fallback | ||
| if interface_lang not in LANGUAGE_TRANSLATIONS[lang_code]: | ||
| interface_lang = 'en' | ||
|
|
||
| # Retorna a tradução (lazy_gettext precisa ser convertido para string) | ||
| translation = LANGUAGE_TRANSLATIONS[lang_code].get(interface_lang, lang_code.upper()) |
There was a problem hiding this comment.
There's a potential inconsistency in how language codes are handled. The function assumes all entries in LANGUAGE_TRANSLATIONS have all three interface languages (pt, en, es), but if a language entry is missing one of these keys, the code falls back to English (line 834) and then uses .get() with a fallback (line 837). This double fallback is redundant. Once interface_lang is set to 'en' on line 834, the .get() on line 837 will always succeed (assuming 'en' key exists in all language entries). Consider either simplifying this logic or adding validation that all language entries have all three interface languages.
| # Se o idioma da interface não está disponível, usa inglês como fallback | |
| if interface_lang not in LANGUAGE_TRANSLATIONS[lang_code]: | |
| interface_lang = 'en' | |
| # Retorna a tradução (lazy_gettext precisa ser convertido para string) | |
| translation = LANGUAGE_TRANSLATIONS[lang_code].get(interface_lang, lang_code.upper()) | |
| lang_translations = LANGUAGE_TRANSLATIONS[lang_code] | |
| # Se o idioma da interface não está disponível, usa inglês como fallback (se existir) | |
| if interface_lang not in lang_translations and 'en' in lang_translations: | |
| interface_lang = 'en' | |
| # Retorna a tradução (lazy_gettext precisa ser convertido para string) | |
| translation = lang_translations.get(interface_lang, lang_code.upper()) |
robertatakenaka
left a comment
There was a problem hiding this comment.
@Rossi-Luciano Realizar as correções
O que esse PR faz?
Este PR resolve o problema de internacionalização onde idiomas além de português, inglês e espanhol apareciam como códigos ISO brutos nos menus de artigos, em vez de nomes traduzidos de acordo com o idioma da interface do usuário.
Problemas resolvidos:
Features implementadas:
LANGUAGE_TRANSLATIONSemchoices.pyg.interface_languageem vez de cookies)Resultado:
Resumo (Alemão),Texto (Francês),PDF (Russo)Abstract (German),Text (French),PDF (Russian)Resumen (Alemán),Texto (Francés),PDF (Ruso)Onde a revisão poderia começar?
Ordem sugerida de revisão:
webapp/choices.pyLANGUAGE_TRANSLATIONS{codigo_iso: {idioma_interface: nome_traduzido}}lazy_gettextpara i18nwebapp/utils/utils.pyget_language_name(lang_code, interface_lang='en')get_language_code_upper(lang_code)register_language_filters(app)webapp/templates/levelMenu_abstracts.htmllanguage_nameno dropdown expandido|upperno botão colapsadog.interface_language(compatível com PR feat: Implementa detecção de idioma via Accept-Language header para habilitar cache no CDN #370)webapp/templates/levelMenu_texts.htmlwebapp/templates/levelMenu_pdf.htmlbtnGroupDropPDFSinglepara PDF únicobtnGroupDropPDFMultiplepara múltiplos PDFslabelledbycorrespondentesComo este poderia ser testado manualmente?
Pré-requisitos:
Teste 1: Verificar tradução em português
Teste 2: Verificar tradução em inglês
Teste 3: Verificar tradução em espanhol
Teste 4: Testar alternância de idioma
Teste 5: Validar HTML (IDs únicos)
btnGroupDropPDFbtnGroupDropPDFSingleOUbtnGroupDropPDFMultipleTeste 6: Fallback para idioma desconhecido
Teste 7: Verificar cache CDN (se aplicável)
Algum cenário de contexto que queira dar?
Contexto do Problema
O site www.scielo.br publica artigos científicos em diversos idiomas além de português, inglês e espanhol. Periódicos internacionais frequentemente incluem resumos, textos completos e PDFs em idiomas como alemão, francês, russo, japonês, chinês, entre outros.
Sistema anterior (hardcoded):
Problemas:
Solução implementada:
Alinhamento com PR #370
Este PR está preparado para o PR #370 que migra detecção de idioma de cookies para Accept-Language header:
g.interface_languageem vez desession.get('lang')Vary: Accept-Languageem vez deVary: CookieImpacto nos Usuários
Pesquisadores internacionais:
Editores de periódicos:
Desenvolvedores:
Screenshots
Antes (Problema)
Interface em Português:
Interface em Inglês:
Depois (Corrigido)
Interface em Português:
Interface em Inglês:
Interface em Espanhol:
Quais são tickets relevantes?
Ticket principal:
Tickets relacionados:
Issues relacionadas no vídeo de demonstração:
Artigos de exemplo mencionados no ticket: