Make site section vocabulary configurable and simplify settings form

Add a vocabulary selector to the settings form so the site section
vocabulary is no longer hardcoded. Remove node bundle checkboxes
(section_page/content_page are always included), keep user and group
as configurable parent targets. All hardcoded 'site_sections'
references in PHP replaced with dynamic config reads via helper.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 13:22:57 -03:00
parent ffca330c89
commit c1274bf3ce
9 changed files with 139 additions and 81 deletions

View File

@@ -1,10 +1,11 @@
site_section_vocabulary: site_sections
allowed_parent_targets: allowed_parent_targets:
- entity_type: node
bundle: content_page
- entity_type: node
bundle: section_page
- entity_type: taxonomy_term - entity_type: taxonomy_term
bundle: site_sections bundle: site_sections
- entity_type: node
bundle: section_page
- entity_type: node
bundle: content_page
- entity_type: user - entity_type: user
bundle: user bundle: user
- entity_type: group - entity_type: group

View File

@@ -2,6 +2,9 @@ structural_pages.settings:
type: config_object type: config_object
label: 'Structural Pages settings' label: 'Structural Pages settings'
mapping: mapping:
site_section_vocabulary:
type: string
label: 'Site section vocabulary'
allowed_parent_targets: allowed_parent_targets:
type: sequence type: sequence
label: 'Allowed parent targets' label: 'Allowed parent targets'

View File

@@ -169,7 +169,7 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
]; ];
} }
// If parent is taxonomy_term, that's our context (handled via site_sections). // If parent is taxonomy_term, that's our context (handled via configured vocabulary).
if ($parent_entity_type === 'taxonomy_term') { if ($parent_entity_type === 'taxonomy_term') {
return [ return [
'type' => 'taxonomy_term', 'type' => 'taxonomy_term',

View File

@@ -105,34 +105,39 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
foreach ($allowed_targets as $target) { foreach ($allowed_targets as $target) {
$key = $target['entity_type'] . ':' . $target['bundle']; $key = $target['entity_type'] . ':' . $target['bundle'];
$enabled_targets[$key] = TRUE; $enabled_targets[$key] = TRUE;
// Handle wildcard bundles.
if ($target['bundle'] === '*') {
$enabled_targets[$target['entity_type'] . ':*'] = TRUE;
}
} }
$form['description'] = [ // Site section vocabulary selector.
'#type' => 'markup', $vocabularies = $this->entityTypeManager->getStorage('taxonomy_vocabulary')->loadMultiple();
'#markup' => '<p>' . $this->t('Select which entity types and bundles can be used as parent for Content Page nodes. This allows creating hierarchical structures where content pages can be children of different entity types.') . '</p>', $vocabulary_options = [];
]; foreach ($vocabularies as $vid => $vocabulary) {
$vocabulary_options[$vid] = $vocabulary->label();
$form['context_info'] = [ }
'#type' => 'markup',
'#markup' => '<p>' . $this->t('<strong>Context behavior:</strong><br> $form['site_section_vocabulary'] = [
- <em>Node/Taxonomy</em>: Content pages inherit the site section from the parent.<br> '#type' => 'select',
- <em>User</em>: Content pages are associated with the user profile page.<br> '#title' => $this->t('Site section vocabulary'),
- <em>Group</em>: Content pages are associated with the group.') . '</p>', '#description' => $this->t('Select the taxonomy vocabulary used for site sections. Content pages and section pages will reference terms from this vocabulary.'),
'#options' => $vocabulary_options,
'#default_value' => $config->get('site_section_vocabulary') ?? 'site_sections',
'#required' => TRUE,
]; ];
// Additional parent targets (user, group).
$form['allowed_parent_targets'] = [ $form['allowed_parent_targets'] = [
'#type' => 'fieldset', '#type' => 'fieldset',
'#title' => $this->t('Allowed Parent Targets'), '#title' => $this->t('Additional Parent Targets'),
'#tree' => TRUE, '#tree' => TRUE,
]; ];
$supportedEntityTypes = $this->handlerManager->getSupportedEntityTypes(); $supportedEntityTypes = $this->handlerManager->getSupportedEntityTypes();
foreach ($supportedEntityTypes as $entity_type => $label) { foreach ($supportedEntityTypes as $entity_type => $label) {
// Skip taxonomy_term and node — managed automatically.
if ($entity_type === 'taxonomy_term' || $entity_type === 'node') {
continue;
}
$form['allowed_parent_targets'][$entity_type] = [ $form['allowed_parent_targets'][$entity_type] = [
'#type' => 'details', '#type' => 'details',
'#title' => $label, '#title' => $label,
@@ -190,11 +195,23 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function submitForm(array &$form, FormStateInterface $form_state): void { public function submitForm(array &$form, FormStateInterface $form_state): void {
$targets = []; $vocabulary = $form_state->getValue('site_section_vocabulary');
$values = $form_state->getValue('allowed_parent_targets'); $values = $form_state->getValue('allowed_parent_targets') ?? [];
$supportedEntityTypes = $this->handlerManager->getSupportedEntityTypes(); $supportedEntityTypes = $this->handlerManager->getSupportedEntityTypes();
// Fixed targets: taxonomy vocabulary + module content types.
$targets = [
['entity_type' => 'taxonomy_term', 'bundle' => $vocabulary],
['entity_type' => 'node', 'bundle' => 'section_page'],
['entity_type' => 'node', 'bundle' => 'content_page'],
];
// Configurable targets: user, group, etc.
foreach ($supportedEntityTypes as $entity_type => $label) { foreach ($supportedEntityTypes as $entity_type => $label) {
if ($entity_type === 'taxonomy_term' || $entity_type === 'node') {
continue;
}
if (empty($values[$entity_type])) { if (empty($values[$entity_type])) {
continue; continue;
} }
@@ -209,7 +226,6 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
} }
foreach ($values[$entity_type] as $bundle => $enabled) { foreach ($values[$entity_type] as $bundle => $enabled) {
// Skip the special "_all" key for groups if not enabled.
if ($bundle === '_all') { if ($bundle === '_all') {
continue; continue;
} }
@@ -223,12 +239,16 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
} }
$this->config('structural_pages.settings') $this->config('structural_pages.settings')
->set('site_section_vocabulary', $vocabulary)
->set('allowed_parent_targets', $targets) ->set('allowed_parent_targets', $targets)
->save(); ->save();
// Update field configuration to reflect new targets. // Update field configuration to reflect new targets.
$this->updateFieldConfiguration($targets); $this->updateFieldConfiguration($targets);
// Update field_site_section target_bundles on both content types.
$this->updateSiteSectionFieldConfiguration($vocabulary);
parent::submitForm($form, $form_state); parent::submitForm($form, $form_state);
} }
@@ -297,4 +317,31 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
$this->messenger()->addStatus($this->t('Field configuration updated successfully.')); $this->messenger()->addStatus($this->t('Field configuration updated successfully.'));
} }
/**
* Updates field_site_section target_bundles on both content types.
*
* @param string $vocabulary
* The vocabulary machine name to set as target bundle.
*/
protected function updateSiteSectionFieldConfiguration(string $vocabulary): void {
$field_names = [
'node.section_page.field_site_section',
'node.content_page.field_site_section',
];
$storage = $this->entityTypeManager->getStorage('field_config');
foreach ($field_names as $field_name) {
$field_config = $storage->load($field_name);
if (!$field_config) {
continue;
}
$settings = $field_config->getSettings();
$settings['handler_settings']['target_bundles'] = [$vocabulary => $vocabulary];
$field_config->setSettings($settings);
$field_config->save();
}
}
} }

View File

@@ -20,7 +20,7 @@ use Drupal\taxonomy\TermInterface;
entity_type_id: 'taxonomy_term', entity_type_id: 'taxonomy_term',
clears_site_section: FALSE, clears_site_section: FALSE,
sort_field: 'name', sort_field: 'name',
bundle_restrictions: ['site_sections'], bundle_restrictions: [],
weight: 0, weight: 0,
)] )]
class TaxonomyTermHandler extends ParentEntityHandlerBase { class TaxonomyTermHandler extends ParentEntityHandlerBase {
@@ -61,6 +61,19 @@ class TaxonomyTermHandler extends ParentEntityHandlerBase {
} }
} }
/**
* {@inheritdoc}
*/
public function handlesEntity(EntityInterface $entity): bool {
if ($entity->getEntityTypeId() !== 'taxonomy_term') {
return FALSE;
}
$vocabulary = \Drupal::config('structural_pages.settings')
->get('site_section_vocabulary') ?? 'site_sections';
return $entity->bundle() === $vocabulary;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@@ -69,8 +82,7 @@ class TaxonomyTermHandler extends ParentEntityHandlerBase {
return NULL; return NULL;
} }
// Only site_sections terms can be site sections. if (!$this->handlesEntity($entity)) {
if ($entity->bundle() !== 'site_sections') {
return NULL; return NULL;
} }

View File

@@ -13,7 +13,7 @@ use Drupal\taxonomy\Entity\Term;
* Implements hook_install(). * Implements hook_install().
*/ */
function structural_pages_install(): void { function structural_pages_install(): void {
// Create default terms for site_sections vocabulary. // Create default terms for the configured vocabulary.
_structural_pages_create_default_terms(); _structural_pages_create_default_terms();
// Display success message. // Display success message.
@@ -21,10 +21,10 @@ function structural_pages_install(): void {
} }
/** /**
* Creates default terms for the site_sections vocabulary. * Creates default terms for the configured site section vocabulary.
*/ */
function _structural_pages_create_default_terms(): void { function _structural_pages_create_default_terms(): void {
$vocabulary = 'site_sections'; $vocabulary = _structural_pages_get_vocabulary();
// Check if terms already exist (avoid duplication on reinstall). // Check if terms already exist (avoid duplication on reinstall).
$existing = \Drupal::entityQuery('taxonomy_term') $existing = \Drupal::entityQuery('taxonomy_term')
@@ -210,10 +210,11 @@ function structural_pages_uninstall(): void {
} }
} }
// Remove terms from site_sections vocabulary. // Remove terms from the configured vocabulary.
$vocabulary = _structural_pages_get_vocabulary();
$tids = \Drupal::entityQuery('taxonomy_term') $tids = \Drupal::entityQuery('taxonomy_term')
->accessCheck(FALSE) ->accessCheck(FALSE)
->condition('vid', 'site_sections') ->condition('vid', $vocabulary)
->execute(); ->execute();
if ($tids) { if ($tids) {
$terms = $entity_type_manager->getStorage('taxonomy_term')->loadMultiple($tids); $terms = $entity_type_manager->getStorage('taxonomy_term')->loadMultiple($tids);
@@ -227,7 +228,7 @@ function structural_pages_uninstall(): void {
// Pathauto patterns. // Pathauto patterns.
'pathauto.pattern.section_page', 'pathauto.pattern.section_page',
'pathauto.pattern.content_page', 'pathauto.pattern.content_page',
'pathauto.pattern.site_sections_term', 'pathauto.pattern.' . $vocabulary . '_term',
// Entity displays. // Entity displays.
'core.entity_form_display.node.section_page.default', 'core.entity_form_display.node.section_page.default',
'core.entity_view_display.node.section_page.default', 'core.entity_view_display.node.section_page.default',
@@ -246,7 +247,7 @@ function structural_pages_uninstall(): void {
'node.type.section_page', 'node.type.section_page',
'node.type.content_page', 'node.type.content_page',
// Vocabulary. // Vocabulary.
'taxonomy.vocabulary.site_sections', 'taxonomy.vocabulary.' . $vocabulary,
]; ];
$config_factory = \Drupal::configFactory(); $config_factory = \Drupal::configFactory();
@@ -290,15 +291,16 @@ function structural_pages_update_10003(): void {
} }
/** /**
* Fix content translation config and update site_sections term translations. * Fix content translation config and update term translations.
*/ */
function structural_pages_update_10004(): void { function structural_pages_update_10004(): void {
// Ensure content translation is properly configured for the vocabulary. // Ensure content translation is properly configured for the vocabulary.
_structural_pages_ensure_content_translation(); _structural_pages_ensure_content_translation();
$vocabulary = _structural_pages_get_vocabulary();
$terms = \Drupal::entityTypeManager() $terms = \Drupal::entityTypeManager()
->getStorage('taxonomy_term') ->getStorage('taxonomy_term')
->loadByProperties(['vid' => 'site_sections']); ->loadByProperties(['vid' => $vocabulary]);
if ($terms) { if ($terms) {
_structural_pages_add_term_translations($terms); _structural_pages_add_term_translations($terms);
@@ -306,14 +308,15 @@ function structural_pages_update_10004(): void {
} }
/** /**
* Ensures content translation settings exist for site_sections vocabulary. * Ensures content translation settings exist for the configured vocabulary.
*/ */
function _structural_pages_ensure_content_translation(): void { function _structural_pages_ensure_content_translation(): void {
if (!\Drupal::moduleHandler()->moduleExists('content_translation')) { if (!\Drupal::moduleHandler()->moduleExists('content_translation')) {
return; return;
} }
$config = \Drupal\language\Entity\ContentLanguageSettings::loadByEntityTypeBundle('taxonomy_term', 'site_sections'); $vocabulary = _structural_pages_get_vocabulary();
$config = \Drupal\language\Entity\ContentLanguageSettings::loadByEntityTypeBundle('taxonomy_term', $vocabulary);
$config->setDefaultLangcode('site_default'); $config->setDefaultLangcode('site_default');
$config->setLanguageAlterable(TRUE); $config->setLanguageAlterable(TRUE);
$config->setThirdPartySetting('content_translation', 'enabled', TRUE); $config->setThirdPartySetting('content_translation', 'enabled', TRUE);
@@ -327,19 +330,20 @@ function structural_pages_requirements(string $phase): array {
$requirements = []; $requirements = [];
if ($phase === 'runtime') { if ($phase === 'runtime') {
// Check if site_sections vocabulary has terms. // Check if the configured vocabulary has terms.
$vocabulary = _structural_pages_get_vocabulary();
$term_count = \Drupal::entityQuery('taxonomy_term') $term_count = \Drupal::entityQuery('taxonomy_term')
->accessCheck(FALSE) ->accessCheck(FALSE)
->condition('vid', 'site_sections') ->condition('vid', $vocabulary)
->count() ->count()
->execute(); ->execute();
if ($term_count === 0) { if ($term_count === 0) {
$requirements['structural_pages_terms'] = [ $requirements['structural_pages_terms'] = [
'title' => t('Structural Pages'), 'title' => t('Structural Pages'),
'value' => t('No terms in site_sections vocabulary'), 'value' => t('No terms in @vocabulary vocabulary', ['@vocabulary' => $vocabulary]),
'description' => t('The Structural Pages module requires terms in the "Site Sections" vocabulary. <a href=":url">Add terms</a>.', [ 'description' => t('The Structural Pages module requires terms in the site section vocabulary. <a href=":url">Add terms</a>.', [
':url' => '/admin/structure/taxonomy/manage/site_sections/add', ':url' => '/admin/structure/taxonomy/manage/' . $vocabulary . '/add',
]), ]),
'severity' => REQUIREMENT_WARNING, 'severity' => REQUIREMENT_WARNING,
]; ];

View File

@@ -1,6 +1,6 @@
structural_pages.settings: structural_pages.settings:
title: 'Structural Pages' title: 'Structural Pages'
description: 'Configure allowed parent entity types for content pages.' description: 'Configure the Structural Pages module settings.'
route_name: structural_pages.settings route_name: structural_pages.settings
parent: site_tools.admin_config parent: site_tools.admin_config
weight: 0 weight: 0

View File

@@ -67,6 +67,17 @@ function structural_pages_entity_presave(EntityInterface $entity): void {
} }
} }
/**
* Returns the configured site section vocabulary machine name.
*
* @return string
* The vocabulary ID (e.g., 'site_sections').
*/
function _structural_pages_get_vocabulary(): string {
return \Drupal::config('structural_pages.settings')
->get('site_section_vocabulary') ?? 'site_sections';
}
/** /**
* Gets the site section ID from a parent entity. * Gets the site section ID from a parent entity.
* *
@@ -82,9 +93,9 @@ function _structural_pages_get_section_from_parent(string $parent_entity_type, i
$entity_type_manager = \Drupal::entityTypeManager(); $entity_type_manager = \Drupal::entityTypeManager();
if ($parent_entity_type === 'taxonomy_term') { if ($parent_entity_type === 'taxonomy_term') {
// If parent is a taxonomy term, verify it's from site_sections vocabulary. // If parent is a taxonomy term, verify it's from the configured vocabulary.
$term = $entity_type_manager->getStorage('taxonomy_term')->load($parent_id); $term = $entity_type_manager->getStorage('taxonomy_term')->load($parent_id);
if ($term instanceof TermInterface && $term->bundle() === 'site_sections') { if ($term instanceof TermInterface && $term->bundle() === _structural_pages_get_vocabulary()) {
return $term->id(); return $term->id();
} }
return NULL; return NULL;
@@ -224,7 +235,7 @@ function structural_pages_token_info(): array {
$info['tokens']['node']['site-section-path'] = [ $info['tokens']['node']['site-section-path'] = [
'name' => t('Site Section Path'), 'name' => t('Site Section Path'),
'description' => t('The hierarchical path of the site_sections taxonomy (e.g., undergraduate/courses).'), 'description' => t('The hierarchical path of the site section taxonomy (e.g., undergraduate/courses).'),
]; ];
$info['tokens']['term']['hierarchy-path'] = [ $info['tokens']['term']['hierarchy-path'] = [

View File

@@ -134,12 +134,12 @@ msgid "Deleted @count @bundle nodes."
msgstr "@count nós do tipo @bundle excluídos." msgstr "@count nós do tipo @bundle excluídos."
#: structural_pages.install #: structural_pages.install
msgid "No terms in site_sections vocabulary" msgid "No terms in @vocabulary vocabulary"
msgstr "Nenhum termo no vocabulário site_sections" msgstr "Nenhum termo no vocabulário @vocabulary"
#: structural_pages.install #: structural_pages.install
msgid "The Structural Pages module requires terms in the \"Site Sections\" vocabulary. <a href=\":url\">Add terms</a>." msgid "The Structural Pages module requires terms in the site section vocabulary. <a href=\":url\">Add terms</a>."
msgstr "O módulo Páginas Estruturais requer termos no vocabulário \"Seções do Site\". <a href=\":url\">Adicionar termos</a>." msgstr "O módulo Páginas Estruturais requer termos no vocabulário de seções do site. <a href=\":url\">Adicionar termos</a>."
#: structural_pages.install #: structural_pages.install
msgid "@count terms configured" msgid "@count terms configured"
@@ -154,8 +154,8 @@ msgid "Site Section Path"
msgstr "Caminho da Seção do Site" msgstr "Caminho da Seção do Site"
#: structural_pages.module #: structural_pages.module
msgid "The hierarchical path of the site_sections taxonomy (e.g., undergraduate/courses)." msgid "The hierarchical path of the site section taxonomy (e.g., undergraduate/courses)."
msgstr "O caminho hierárquico da taxonomia site_sections (ex: graduacao/cursos)." msgstr "O caminho hierárquico da taxonomia de seções do site (ex: graduacao/cursos)."
#: structural_pages.module #: structural_pages.module
msgid "Hierarchy Path" msgid "Hierarchy Path"
@@ -174,32 +174,16 @@ msgid "Structural Pages Settings"
msgstr "Configurações das Páginas Estruturais" msgstr "Configurações das Páginas Estruturais"
#: src/Form/StructuralPagesSettingsForm.php #: src/Form/StructuralPagesSettingsForm.php
msgid "Select which entity types and bundles can be used as parent for Content Page nodes. This allows creating hierarchical structures where content pages can be children of different entity types." msgid "Site section vocabulary"
msgstr "Selecione quais tipos de entidade e bundles podem ser usados como pai para nós de Página de Conteúdo. Isso permite criar estruturas hierárquicas onde páginas de conteúdo podem ser filhas de diferentes tipos de entidade." msgstr "Vocabulário de seções do site"
#: src/Form/StructuralPagesSettingsForm.php #: src/Form/StructuralPagesSettingsForm.php
msgid "<strong>Context behavior:</strong><br>\n - <em>Node/Taxonomy</em>: Content pages inherit the site section from the parent.<br>\n - <em>User</em>: Content pages are associated with the user profile page.<br>\n - <em>Group</em>: Content pages are associated with the group." msgid "Select the taxonomy vocabulary used for site sections. Content pages and section pages will reference terms from this vocabulary."
msgstr "<strong>Comportamento de contexto:</strong><br>\n - <em>Node/Taxonomia</em>: Páginas de conteúdo herdam a seção do site do pai.<br>\n - <em>Usuário</em>: Páginas de conteúdo são associadas à página de perfil do usuário.<br>\n - <em>Grupo</em>: Páginas de conteúdo são associadas ao grupo." msgstr "Selecione o vocabulário de taxonomia usado para seções do site. Páginas de conteúdo e páginas institucionais referenciarão termos deste vocabulário."
#: src/Form/StructuralPagesSettingsForm.php #: src/Form/StructuralPagesSettingsForm.php
msgid "Allowed Parent Targets" msgid "Additional Parent Targets"
msgstr "Alvos de Pai Permitidos" msgstr "Alvos de Pai Adicionais"
#: src/Form/StructuralPagesSettingsForm.php
msgid "Content Types (node)"
msgstr "Tipos de Conteúdo (node)"
#: src/Form/StructuralPagesSettingsForm.php
msgid "Taxonomy Vocabularies (taxonomy_term)"
msgstr "Vocabulários de Taxonomia (taxonomy_term)"
#: src/Form/StructuralPagesSettingsForm.php
msgid "Users (user)"
msgstr "Usuários (user)"
#: src/Form/StructuralPagesSettingsForm.php
msgid "Groups (group)"
msgstr "Grupos (group)"
#: src/Form/StructuralPagesSettingsForm.php #: src/Form/StructuralPagesSettingsForm.php
msgid "User accounts" msgid "User accounts"
@@ -222,8 +206,8 @@ msgid "Field configuration updated successfully."
msgstr "Configuração do campo atualizada com sucesso." msgstr "Configuração do campo atualizada com sucesso."
#: structural_pages.links.menu.yml #: structural_pages.links.menu.yml
msgid "Configure allowed parent entity types for content pages." msgid "Configure the Structural Pages module settings."
msgstr "Configure os tipos de entidade pai permitidos para páginas de conteúdo." msgstr "Configure as opções do módulo Páginas Estruturais."
#: src/Plugin/Block/StructuralPagesMenuBlock.php #: src/Plugin/Block/StructuralPagesMenuBlock.php
msgid "Structural Pages Menu" msgid "Structural Pages Menu"
@@ -258,22 +242,18 @@ msgid "Site structure navigation"
msgstr "Navegação da estrutura do site" msgstr "Navegação da estrutura do site"
#: src/Plugin/ParentEntityHandler/TaxonomyTermHandler.php #: src/Plugin/ParentEntityHandler/TaxonomyTermHandler.php
#: src/Form/StructuralPagesSettingsForm.php
msgid "Taxonomy Vocabularies (taxonomy_term)" msgid "Taxonomy Vocabularies (taxonomy_term)"
msgstr "Vocabulários de Taxonomia (taxonomy_term)" msgstr "Vocabulários de Taxonomia (taxonomy_term)"
#: src/Plugin/ParentEntityHandler/UserHandler.php #: src/Plugin/ParentEntityHandler/UserHandler.php
#: src/Form/StructuralPagesSettingsForm.php
msgid "Users (user)" msgid "Users (user)"
msgstr "Usuários (user)" msgstr "Usuários (user)"
#: src/Plugin/ParentEntityHandler/NodeHandler.php #: src/Plugin/ParentEntityHandler/NodeHandler.php
#: src/Form/StructuralPagesSettingsForm.php
msgid "Content Types (node)" msgid "Content Types (node)"
msgstr "Tipos de Conteúdo (node)" msgstr "Tipos de Conteúdo (node)"
#: modules/structural_pages_group/src/Plugin/ParentEntityHandler/GroupHandler.php #: modules/structural_pages_group/src/Plugin/ParentEntityHandler/GroupHandler.php
#: src/Form/StructuralPagesSettingsForm.php
msgid "Groups (group)" msgid "Groups (group)"
msgstr "Grupos (group)" msgstr "Grupos (group)"