From e815b370e8b11ea8f47662f5a1513b398562224c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:31 -0300 Subject: [PATCH 01/18] fix(l10n): adjust field definition service messages Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/FieldDefinitionService.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Service/FieldDefinitionService.php b/lib/Service/FieldDefinitionService.php index 8638c76..8ae80ca 100644 --- a/lib/Service/FieldDefinitionService.php +++ b/lib/Service/FieldDefinitionService.php @@ -32,7 +32,7 @@ public function __construct( public function create(array $definition): FieldDefinition { $validated = $this->validator->validate($definition); if ($this->fieldDefinitionMapper->findByFieldKey($validated['field_key']) !== null) { - throw new InvalidArgumentException($this->l10n->t('field_key already exists')); + throw new InvalidArgumentException($this->l10n->t('"field_key" already exists')); } $createdAt = $this->parseImportedDate($definition['created_at'] ?? null) ?? new DateTime(); @@ -48,7 +48,7 @@ public function create(array $definition): FieldDefinition { try { $entity->setOptions(isset($validated['options']) ? json_encode($validated['options'], JSON_THROW_ON_ERROR) : null); } catch (JsonException $e) { - throw new InvalidArgumentException($this->l10n->t('options could not be encoded: %s', [$e->getMessage()]), 0, $e); + throw new InvalidArgumentException($this->l10n->t('Options could not be encoded: %s', [$e->getMessage()]), 0, $e); } $entity->setCreatedAt($createdAt); $entity->setUpdatedAt($updatedAt); @@ -62,11 +62,11 @@ public function create(array $definition): FieldDefinition { public function update(FieldDefinition $existing, array $definition): FieldDefinition { $validated = $this->validator->validate($definition + ['field_key' => $existing->getFieldKey()]); if (($definition['field_key'] ?? $existing->getFieldKey()) !== $existing->getFieldKey()) { - throw new InvalidArgumentException($this->l10n->t('field_key cannot be changed')); + throw new InvalidArgumentException($this->l10n->t('"field_key" cannot be changed')); } if ($validated['type'] !== $existing->getType() && $this->fieldValueMapper->hasValuesForFieldDefinitionId($existing->getId())) { - throw new InvalidArgumentException($this->l10n->t('type cannot be changed after values exist')); + throw new InvalidArgumentException($this->l10n->t('Type cannot be changed after values exist')); } $existing->setLabel($validated['label']); @@ -78,7 +78,7 @@ public function update(FieldDefinition $existing, array $definition): FieldDefin try { $existing->setOptions(isset($validated['options']) ? json_encode($validated['options'], JSON_THROW_ON_ERROR) : null); } catch (JsonException $e) { - throw new InvalidArgumentException($this->l10n->t('options could not be encoded: %s', [$e->getMessage()]), 0, $e); + throw new InvalidArgumentException($this->l10n->t('Options could not be encoded: %s', [$e->getMessage()]), 0, $e); } $existing->setUpdatedAt($this->parseImportedDate($definition['updated_at'] ?? null) ?? new DateTime()); From 2f3bc370fe89b0a981eb6a6dce5d90508c65f275 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:31 -0300 Subject: [PATCH 02/18] fix(l10n): capitalize field value limit validation message Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/FieldValueService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/FieldValueService.php b/lib/Service/FieldValueService.php index fa70c2d..d00ab59 100644 --- a/lib/Service/FieldValueService.php +++ b/lib/Service/FieldValueService.php @@ -151,7 +151,7 @@ public function searchByDefinition( ): array { if ($limit < 1 || $limit > self::SEARCH_MAX_LIMIT) { // TRANSLATORS %d is the maximum supported search limit. - throw new InvalidArgumentException($this->l10n->t('limit must be between 1 and %d', [self::SEARCH_MAX_LIMIT])); + throw new InvalidArgumentException($this->l10n->t('Limit must be between 1 and %d', [self::SEARCH_MAX_LIMIT])); } if ($offset < 0) { From 4bbd840feab1b986739fcde718dbe52609ec660e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:31 -0300 Subject: [PATCH 03/18] fix(l10n): update admin settings status strings Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/AdminSettings.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index 5d0530e..c6b7828 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -397,7 +397,7 @@ const editorEmptyState = computed(() => sortedDefinitions.value.length === 0 description: t('profile_fields', 'Select a field from the list, or create a new one.'), }) const configuredFieldsCountLabel = computed(() => n('profile_fields', 'field configured', 'fields configured', definitions.value.length, { count: definitions.value.length })) -const saveActionLabel = computed(() => isSaving.value ? t('profile_fields', 'Saving changes…') : (isEditing.value ? t('profile_fields', 'Save changes') : t('profile_fields', 'Create field'))) +const saveActionLabel = computed(() => isSaving.value ? t('profile_fields', 'Saving changes\u00A0…') : (isEditing.value ? t('profile_fields', 'Save changes') : t('profile_fields', 'Create field'))) const editFieldAriaLabel = (label: string) => t('profile_fields', 'Edit field {label}', { label }) const actionsForLabel = (label: string) => t('profile_fields', 'Actions for {label}', { label }) const toggleDefinitionActiveLabel = (definition: FieldDefinition) => definition.active @@ -628,7 +628,7 @@ const persistDefinition = async() => { selectedId.value = created.id populateForm(created) markJustSaved(created.id) - setSuccessMessage(t('profile_fields', 'Field created successfully.')) + setSuccessMessage(t('profile_fields', 'Field created.')) } else { const updated = await updateDefinition(selectedDefinition.value.id, { label: payload.label, @@ -642,7 +642,7 @@ const persistDefinition = async() => { replaceDefinitionInState(updated) populateForm(updated) markJustSaved(updated.id) - setSuccessMessage(t('profile_fields', 'Field updated successfully.')) + setSuccessMessage(t('profile_fields', 'Field updated.')) } if (isCompactLayout.value) { closeEditor() @@ -666,7 +666,7 @@ const removeDefinition = async() => { removeDefinitionFromState(selectedDefinition.value.id) isCreatingNew.value = false resetForm() - setSuccessMessage(t('profile_fields', 'Field deleted successfully.')) + setSuccessMessage(t('profile_fields', 'Field deleted.')) } catch (error: any) { errorMessage.value = error?.response?.data?.ocs?.data?.message ?? error?.message ?? t('profile_fields', 'Could not delete this field. Please try again.') } finally { From 9d87a99aaafb186465ab1acdfe16f907660dabc7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:31 -0300 Subject: [PATCH 04/18] fix(l10n): adjust admin user dialog loading and saving labels Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/AdminUserFieldsDialog.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AdminUserFieldsDialog.vue b/src/components/AdminUserFieldsDialog.vue index 3a2314d..2d396b7 100644 --- a/src/components/AdminUserFieldsDialog.vue +++ b/src/components/AdminUserFieldsDialog.vue @@ -124,7 +124,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later {{ t('profile_fields', 'Cancel') }} - {{ isSavingAny ? t('profile_fields', 'Saving changes…') : t('profile_fields', 'Save changes') }} + {{ isSavingAny ? t('profile_fields', 'Saving changes\u00A0…') : t('profile_fields', 'Save changes') }} @@ -184,7 +184,7 @@ export default defineComponent({ const headerUserName = computed(() => props.userDisplayName.trim() !== '' ? props.userDisplayName : props.userUid) const visibilityFieldLabel = t('profile_fields', 'Who can view this field value') - const loadingMessage = computed(() => t('profile_fields', 'Loading profile fields for {userUid}…', { userUid: props.userUid })) + const loadingMessage = computed(() => t('profile_fields', 'Loading profile fields for {userUid}\u00A0…', { userUid: props.userUid })) const editableFields = computed(() => buildAdminEditableFields(definitions.value, userValues.value)) const isSavingAny = computed(() => savingIds.value.length > 0) const headerDescription = computed(() => { From 14abeda6b5862fce73ec266f3c18d61fabc0c02c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:32 -0300 Subject: [PATCH 05/18] fix(l10n): adjust personal settings saving label Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/PersonalSettings.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/PersonalSettings.vue b/src/views/PersonalSettings.vue index 7a9e26f..9d8e649 100644 --- a/src/views/PersonalSettings.vue +++ b/src/views/PersonalSettings.vue @@ -151,7 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later /> - {{ isSaving(field.definition.id) ? t('profile_fields', 'Saving changes…') : t('profile_fields', 'Save changes') }} + {{ isSaving(field.definition.id) ? t('profile_fields', 'Saving changes\u00A0…') : t('profile_fields', 'Save changes') }} From 6f5140b66c1460fd9c19d859e7a4a51ac6185ed5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:32 -0300 Subject: [PATCH 06/18] fix(l10n): improve option count and plural strings Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/admin/AdminSelectOptionsSection.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/admin/AdminSelectOptionsSection.vue b/src/components/admin/AdminSelectOptionsSection.vue index 35e680f..ebda936 100644 --- a/src/components/admin/AdminSelectOptionsSection.vue +++ b/src/components/admin/AdminSelectOptionsSection.vue @@ -155,8 +155,8 @@ const createOptionId = () => `option-local-${nextOptionId++}` const options = computed(() => props.modelValue) const bulkOptionValues = computed(() => parseEditableSelectOptionValues(bulkOptionInput.value)) const normalizedOptionCount = computed(() => extractEditableSelectOptionValues(options.value).filter((optionValue: string) => optionValue.trim() !== '').length) -const optionsCountLabel = computed(() => n('profile_fields', 'option', 'options', normalizedOptionCount.value, { count: normalizedOptionCount.value })) -const bulkOptionsSummary = computed(() => n('profile_fields', '1 option ready.', '{count} options ready.', bulkOptionValues.value.length, { count: bulkOptionValues.value.length })) +const optionsCountLabel = computed(() => n('profile_fields', 'Option', 'Options', normalizedOptionCount.value, { count: normalizedOptionCount.value })) +const bulkOptionsSummary = computed(() => n('profile_fields', '{count} option ready.', '{count} options ready.', bulkOptionValues.value.length, { count: bulkOptionValues.value.length })) const duplicateOptionIndices = computed(() => { const seen = new Map() From 2af74931edd706e6697a50019f1267b2bddee228 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:32 -0300 Subject: [PATCH 07/18] fix(l10n): update workflow loading placeholder spacing Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/workflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workflow.ts b/src/workflow.ts index 34c925d..d24e739 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -536,7 +536,7 @@ class WorkflowProfileFieldElement extends HTMLElement { const placeholder = document.createElement('option') placeholder.value = '' placeholder.textContent = definitions.length === 0 - ? t('profile_fields', 'Loading profile fields…') + ? t('profile_fields', 'Loading profile fields\u00A0…') : t('profile_fields', 'Choose a profile field') fieldSelect.append(placeholder) From 319c463abfae04d417db14fd9e2a565b8e197b2f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:32 -0300 Subject: [PATCH 08/18] test(php): sync field definition service message expectations Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/FieldDefinitionServiceTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/php/Unit/Service/FieldDefinitionServiceTest.php b/tests/php/Unit/Service/FieldDefinitionServiceTest.php index c0e4793..6c8657a 100644 --- a/tests/php/Unit/Service/FieldDefinitionServiceTest.php +++ b/tests/php/Unit/Service/FieldDefinitionServiceTest.php @@ -51,7 +51,7 @@ public function testCreateRejectsDuplicatedFieldKey(): void { ->willReturn(new FieldDefinition()); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('field_key already exists'); + $this->expectExceptionMessage('"field_key" already exists'); $this->service->create([ 'field_key' => 'cpf', @@ -127,7 +127,7 @@ public function testUpdateRejectsFieldKeyRename(): void { $existing->setExposurePolicy(FieldExposurePolicy::PRIVATE->value); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('field_key cannot be changed'); + $this->expectExceptionMessage('"field_key" cannot be changed'); $this->service->update($existing, [ 'field_key' => 'cpf_new', @@ -150,7 +150,7 @@ public function testUpdateRejectsTypeChangeWhenValuesExist(): void { ->willReturn(true); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('type cannot be changed after values exist'); + $this->expectExceptionMessage('Type cannot be changed after values exist'); $this->service->update($existing, [ 'label' => 'CPF', From cbbbb4cfd69c0045af82fabfbba4adac2e64b889 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:32 -0300 Subject: [PATCH 09/18] test(php): sync api controller validation message expectation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../php/Unit/Controller/FieldDefinitionApiControllerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/php/Unit/Controller/FieldDefinitionApiControllerTest.php b/tests/php/Unit/Controller/FieldDefinitionApiControllerTest.php index 2369d76..ae8fca0 100644 --- a/tests/php/Unit/Controller/FieldDefinitionApiControllerTest.php +++ b/tests/php/Unit/Controller/FieldDefinitionApiControllerTest.php @@ -115,7 +115,7 @@ public function testCreateSelectFieldForwardsOptions(): void { public function testCreateReturnsBadRequestOnValidationFailure(): void { $this->service->expects($this->once()) ->method('create') - ->willThrowException(new InvalidArgumentException('field_key already exists')); + ->willThrowException(new InvalidArgumentException('"field_key" already exists')); $response = $this->controller->create( 'cpf', @@ -128,7 +128,7 @@ public function testCreateReturnsBadRequestOnValidationFailure(): void { ); $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus()); - $this->assertSame(['message' => 'field_key already exists'], $response->getData()); + $this->assertSame(['message' => '"field_key" already exists'], $response->getData()); } public function testUpdateSelectFieldForwardsOptions(): void { From 2e31738299973125b9e6607f877ce652870fb767 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:32 -0300 Subject: [PATCH 10/18] test(frontend): update option label expectation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/components/admin/AdminSelectOptionsSection.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/components/admin/AdminSelectOptionsSection.spec.ts b/src/tests/components/admin/AdminSelectOptionsSection.spec.ts index 15f4fd1..713beaf 100644 --- a/src/tests/components/admin/AdminSelectOptionsSection.spec.ts +++ b/src/tests/components/admin/AdminSelectOptionsSection.spec.ts @@ -76,7 +76,7 @@ describe('AdminSelectOptionsSection', () => { }) expect(wrapper.text()).toContain('tr:Options') - expect(wrapper.text()).toContain('tr:option') + expect(wrapper.text()).toContain('tr:Option') expect(wrapper.text()).toContain('tr:Add single option') }) From f3f26384739aeff07480e84b9ff3e20cad790a45 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:13:12 -0300 Subject: [PATCH 11/18] docs(l10n): clarify translator notes for technical placeholders Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/FieldDefinitionService.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Service/FieldDefinitionService.php b/lib/Service/FieldDefinitionService.php index 8ae80ca..70aa7bd 100644 --- a/lib/Service/FieldDefinitionService.php +++ b/lib/Service/FieldDefinitionService.php @@ -32,6 +32,7 @@ public function __construct( public function create(array $definition): FieldDefinition { $validated = $this->validator->validate($definition); if ($this->fieldDefinitionMapper->findByFieldKey($validated['field_key']) !== null) { + // TRANSLATORS "field_key" is a technical API field identifier and should remain unchanged. throw new InvalidArgumentException($this->l10n->t('"field_key" already exists')); } @@ -48,6 +49,7 @@ public function create(array $definition): FieldDefinition { try { $entity->setOptions(isset($validated['options']) ? json_encode($validated['options'], JSON_THROW_ON_ERROR) : null); } catch (JsonException $e) { + // TRANSLATORS %s is a low-level JSON encoder error detail. throw new InvalidArgumentException($this->l10n->t('Options could not be encoded: %s', [$e->getMessage()]), 0, $e); } $entity->setCreatedAt($createdAt); @@ -62,6 +64,7 @@ public function create(array $definition): FieldDefinition { public function update(FieldDefinition $existing, array $definition): FieldDefinition { $validated = $this->validator->validate($definition + ['field_key' => $existing->getFieldKey()]); if (($definition['field_key'] ?? $existing->getFieldKey()) !== $existing->getFieldKey()) { + // TRANSLATORS "field_key" is a technical API field identifier and should remain unchanged. throw new InvalidArgumentException($this->l10n->t('"field_key" cannot be changed')); } @@ -78,6 +81,7 @@ public function update(FieldDefinition $existing, array $definition): FieldDefin try { $existing->setOptions(isset($validated['options']) ? json_encode($validated['options'], JSON_THROW_ON_ERROR) : null); } catch (JsonException $e) { + // TRANSLATORS %s is a low-level JSON encoder error detail. throw new InvalidArgumentException($this->l10n->t('Options could not be encoded: %s', [$e->getMessage()]), 0, $e); } $existing->setUpdatedAt($this->parseImportedDate($definition['updated_at'] ?? null) ?? new DateTime()); From f0d0ca231d74181428131b86ce3a5ce572b835a4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:13:12 -0300 Subject: [PATCH 12/18] docs(l10n): justify NBSP usage in admin user dialog strings Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/AdminUserFieldsDialog.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/AdminUserFieldsDialog.vue b/src/components/AdminUserFieldsDialog.vue index 2d396b7..f5f1b9d 100644 --- a/src/components/AdminUserFieldsDialog.vue +++ b/src/components/AdminUserFieldsDialog.vue @@ -123,6 +123,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later {{ t('profile_fields', 'Cancel') }} + {{ isSavingAny ? t('profile_fields', 'Saving changes\u00A0…') : t('profile_fields', 'Save changes') }} @@ -184,6 +185,7 @@ export default defineComponent({ const headerUserName = computed(() => props.userDisplayName.trim() !== '' ? props.userDisplayName : props.userUid) const visibilityFieldLabel = t('profile_fields', 'Who can view this field value') + // TRANSLATORS "{userUid}" is a technical account identifier (not the display name). "\u00A0" keeps the ellipsis attached to the previous word and avoids awkward line breaks. const loadingMessage = computed(() => t('profile_fields', 'Loading profile fields for {userUid}\u00A0…', { userUid: props.userUid })) const editableFields = computed(() => buildAdminEditableFields(definitions.value, userValues.value)) const isSavingAny = computed(() => savingIds.value.length > 0) From bd5abb80895e06d246323dd1a32f3da3b17b0853 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:13:12 -0300 Subject: [PATCH 13/18] docs(l10n): add translator context for option placeholders Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/admin/AdminSelectOptionsSection.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/admin/AdminSelectOptionsSection.vue b/src/components/admin/AdminSelectOptionsSection.vue index ebda936..5c1ecb3 100644 --- a/src/components/admin/AdminSelectOptionsSection.vue +++ b/src/components/admin/AdminSelectOptionsSection.vue @@ -155,7 +155,9 @@ const createOptionId = () => `option-local-${nextOptionId++}` const options = computed(() => props.modelValue) const bulkOptionValues = computed(() => parseEditableSelectOptionValues(bulkOptionInput.value)) const normalizedOptionCount = computed(() => extractEditableSelectOptionValues(options.value).filter((optionValue: string) => optionValue.trim() !== '').length) +// TRANSLATORS "Option/Options" here means selectable field values, not application settings. const optionsCountLabel = computed(() => n('profile_fields', 'Option', 'Options', normalizedOptionCount.value, { count: normalizedOptionCount.value })) +// TRANSLATORS "{count}" is the number of parsed selectable values ready to be added. const bulkOptionsSummary = computed(() => n('profile_fields', '{count} option ready.', '{count} options ready.', bulkOptionValues.value.length, { count: bulkOptionValues.value.length })) const duplicateOptionIndices = computed(() => { @@ -184,8 +186,11 @@ const hasOptionValue = (index: number) => options.value[index]?.value.trim() !== const canMoveOptionUp = (index: number) => index > 0 const canMoveOptionDown = (index: number) => index < options.value.length - 1 const isOptionDuplicate = (index: number) => duplicateOptionIndices.value.has(index) +// TRANSLATORS "{optionValue}" is the visible text of one selectable option. const reorderOptionLabel = (optionValue: string) => t('profile_fields', 'Reorder option {optionValue}', { optionValue }) +// TRANSLATORS "{position}" is a 1-based option index shown as placeholder text. const optionPlaceholder = (position: number) => t('profile_fields', 'Option {position}', { position }) +// TRANSLATORS "{optionValue}" is the visible text of one selectable option. const removeOptionLabel = (optionValue: string) => t('profile_fields', 'Remove option {optionValue}', { optionValue }) const focusOptionInput = async(index: number) => { From 8ccb3d462ff91d0ad17240d2ad75962720c50ff4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:13:12 -0300 Subject: [PATCH 14/18] docs(l10n): explain field key and NBSP translation context Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/AdminSettings.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index c6b7828..956035d 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -149,6 +149,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
+ + sortedDefinitions.value.length === 0 description: t('profile_fields', 'Select a field from the list, or create a new one.'), }) const configuredFieldsCountLabel = computed(() => n('profile_fields', 'field configured', 'fields configured', definitions.value.length, { count: definitions.value.length })) +// TRANSLATORS "\u00A0" keeps the ellipsis attached to the previous word for correct typography and avoids awkward line breaks. const saveActionLabel = computed(() => isSaving.value ? t('profile_fields', 'Saving changes\u00A0…') : (isEditing.value ? t('profile_fields', 'Save changes') : t('profile_fields', 'Create field'))) const editFieldAriaLabel = (label: string) => t('profile_fields', 'Edit field {label}', { label }) const actionsForLabel = (label: string) => t('profile_fields', 'Actions for {label}', { label }) From 2d53ef751c1d10b3c5a112840a5711671b5768f8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:13:12 -0300 Subject: [PATCH 15/18] docs(l10n): justify NBSP note in personal settings Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/PersonalSettings.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/PersonalSettings.vue b/src/views/PersonalSettings.vue index 9d8e649..64e8891 100644 --- a/src/views/PersonalSettings.vue +++ b/src/views/PersonalSettings.vue @@ -151,6 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later /> + {{ isSaving(field.definition.id) ? t('profile_fields', 'Saving changes\u00A0…') : t('profile_fields', 'Save changes') }}
From 201a750ecbd8b9c6e4aac844ad7e49c009f1f230 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:13:12 -0300 Subject: [PATCH 16/18] docs(l10n): justify NBSP note in workflow placeholder Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/workflow.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/workflow.ts b/src/workflow.ts index d24e739..4c79c48 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -535,6 +535,7 @@ class WorkflowProfileFieldElement extends HTMLElement { const placeholder = document.createElement('option') placeholder.value = '' + // TRANSLATORS "\u00A0" keeps the ellipsis attached to the previous word for correct typography and avoids awkward line breaks. placeholder.textContent = definitions.length === 0 ? t('profile_fields', 'Loading profile fields\u00A0…') : t('profile_fields', 'Choose a profile field') From c743d1a3348491e6b1cc29e566c6cdd18e022cd1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:25:04 -0300 Subject: [PATCH 17/18] test(e2e): sync success toast expectations Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright/e2e/profile-fields.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/playwright/e2e/profile-fields.spec.ts b/playwright/e2e/profile-fields.spec.ts index 2b532e6..a6018f0 100644 --- a/playwright/e2e/profile-fields.spec.ts +++ b/playwright/e2e/profile-fields.spec.ts @@ -95,18 +95,18 @@ test('admin can create, update, and delete a field definition', async ({ page }) await page.locator('#profile-fields-admin-label').fill(createdLabel) await page.getByTestId('profile-fields-admin-save').click() - await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created successfully.') + await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created.') await expect(page.getByTestId(`profile-fields-admin-definition-${fieldKey}`)).toBeVisible() await page.locator('#profile-fields-admin-label').fill(updatedLabel) await page.getByTestId('profile-fields-admin-save').click() - await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated successfully.') + await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated.') await expect(page.getByTestId(`profile-fields-admin-definition-${fieldKey}`)).toContainText(updatedLabel) await page.getByTestId('profile-fields-admin-delete').click() - await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field deleted successfully.') + await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field deleted.') await expect(page.getByTestId(`profile-fields-admin-definition-${fieldKey}`)).toHaveCount(0) await deleteDefinitionByFieldKey(page.request, fieldKey) }) @@ -152,7 +152,7 @@ test('admin uses a modal editor on compact layout', async ({ page }) => { await createDialog.locator('#profile-fields-admin-label').fill(createdLabel) await createDialog.getByTestId('profile-fields-admin-save').click() - await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created successfully.') + await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created.') await expect(page.getByTestId(`profile-fields-admin-definition-${createdFieldKey}`)).toBeVisible() await expect(createDialog).toBeHidden() } finally { @@ -301,7 +301,7 @@ test('admin gets an initial select option row and can remove empty rows by keybo await expect(page.getByTestId('profile-fields-admin-option-handle-0')).toBeVisible() await page.getByTestId('profile-fields-admin-save').click() - await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created successfully.') + await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created.') await deleteDefinitionByFieldKey(page.request, fieldKey) }) @@ -336,7 +336,7 @@ test('admin can bulk add select options from multiple lines', async ({ page }) = } await page.getByTestId('profile-fields-admin-save').click() - await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created successfully.') + await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created.') await deleteDefinitionByFieldKey(page.request, fieldKey) }) @@ -400,7 +400,7 @@ test('admin reuses the empty select option row on repeated Enter', async ({ page await expect(page.getByTestId('profile-fields-admin-option-row-4')).toHaveCount(0) await page.getByTestId('profile-fields-admin-save').click() - await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated successfully.') + await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated.') await page.reload() await openSelectDefinitionEditor(page, fieldKey, label) @@ -451,7 +451,7 @@ test('admin can reorder select options from the handle menu and drag handle', as await expect(optionInput(page, 3)).toHaveValue('Gamma') await page.getByTestId('profile-fields-admin-save').click() - await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated successfully.') + await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated.') await page.reload() await openSelectDefinitionEditor(page, fieldKey, label) From 51be8464023583b879604c10f4b8f49ae48a3f89 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:32:03 -0300 Subject: [PATCH 18/18] docs(l10n): clarify field key translator context scope Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/AdminSettings.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index 956035d..90eb421 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
- +