Skip to content

Commit 363b8e3

Browse files
authored
Merge pull request #80 from LibreCodeCoop/fix/issue-78-l10n-strings
fix(l10n): adapt source strings for translation readiness
2 parents 39787ee + 51be846 commit 363b8e3

File tree

11 files changed

+46
-30
lines changed

11 files changed

+46
-30
lines changed

lib/Service/FieldDefinitionService.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ public function __construct(
3232
public function create(array $definition): FieldDefinition {
3333
$validated = $this->validator->validate($definition);
3434
if ($this->fieldDefinitionMapper->findByFieldKey($validated['field_key']) !== null) {
35-
throw new InvalidArgumentException($this->l10n->t('field_key already exists'));
35+
// TRANSLATORS "field_key" is a technical API field identifier and should remain unchanged.
36+
throw new InvalidArgumentException($this->l10n->t('"field_key" already exists'));
3637
}
3738

3839
$createdAt = $this->parseImportedDate($definition['created_at'] ?? null) ?? new DateTime();
@@ -48,7 +49,8 @@ public function create(array $definition): FieldDefinition {
4849
try {
4950
$entity->setOptions(isset($validated['options']) ? json_encode($validated['options'], JSON_THROW_ON_ERROR) : null);
5051
} catch (JsonException $e) {
51-
throw new InvalidArgumentException($this->l10n->t('options could not be encoded: %s', [$e->getMessage()]), 0, $e);
52+
// TRANSLATORS %s is a low-level JSON encoder error detail.
53+
throw new InvalidArgumentException($this->l10n->t('Options could not be encoded: %s', [$e->getMessage()]), 0, $e);
5254
}
5355
$entity->setCreatedAt($createdAt);
5456
$entity->setUpdatedAt($updatedAt);
@@ -62,11 +64,12 @@ public function create(array $definition): FieldDefinition {
6264
public function update(FieldDefinition $existing, array $definition): FieldDefinition {
6365
$validated = $this->validator->validate($definition + ['field_key' => $existing->getFieldKey()]);
6466
if (($definition['field_key'] ?? $existing->getFieldKey()) !== $existing->getFieldKey()) {
65-
throw new InvalidArgumentException($this->l10n->t('field_key cannot be changed'));
67+
// TRANSLATORS "field_key" is a technical API field identifier and should remain unchanged.
68+
throw new InvalidArgumentException($this->l10n->t('"field_key" cannot be changed'));
6669
}
6770

6871
if ($validated['type'] !== $existing->getType() && $this->fieldValueMapper->hasValuesForFieldDefinitionId($existing->getId())) {
69-
throw new InvalidArgumentException($this->l10n->t('type cannot be changed after values exist'));
72+
throw new InvalidArgumentException($this->l10n->t('Type cannot be changed after values exist'));
7073
}
7174

7275
$existing->setLabel($validated['label']);
@@ -78,7 +81,8 @@ public function update(FieldDefinition $existing, array $definition): FieldDefin
7881
try {
7982
$existing->setOptions(isset($validated['options']) ? json_encode($validated['options'], JSON_THROW_ON_ERROR) : null);
8083
} catch (JsonException $e) {
81-
throw new InvalidArgumentException($this->l10n->t('options could not be encoded: %s', [$e->getMessage()]), 0, $e);
84+
// TRANSLATORS %s is a low-level JSON encoder error detail.
85+
throw new InvalidArgumentException($this->l10n->t('Options could not be encoded: %s', [$e->getMessage()]), 0, $e);
8286
}
8387
$existing->setUpdatedAt($this->parseImportedDate($definition['updated_at'] ?? null) ?? new DateTime());
8488

lib/Service/FieldValueService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public function searchByDefinition(
151151
): array {
152152
if ($limit < 1 || $limit > self::SEARCH_MAX_LIMIT) {
153153
// TRANSLATORS %d is the maximum supported search limit.
154-
throw new InvalidArgumentException($this->l10n->t('limit must be between 1 and %d', [self::SEARCH_MAX_LIMIT]));
154+
throw new InvalidArgumentException($this->l10n->t('Limit must be between 1 and %d', [self::SEARCH_MAX_LIMIT]));
155155
}
156156

157157
if ($offset < 0) {

playwright/e2e/profile-fields.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,18 @@ test('admin can create, update, and delete a field definition', async ({ page })
9595
await page.locator('#profile-fields-admin-label').fill(createdLabel)
9696
await page.getByTestId('profile-fields-admin-save').click()
9797

98-
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created successfully.')
98+
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created.')
9999
await expect(page.getByTestId(`profile-fields-admin-definition-${fieldKey}`)).toBeVisible()
100100

101101
await page.locator('#profile-fields-admin-label').fill(updatedLabel)
102102
await page.getByTestId('profile-fields-admin-save').click()
103103

104-
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated successfully.')
104+
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated.')
105105
await expect(page.getByTestId(`profile-fields-admin-definition-${fieldKey}`)).toContainText(updatedLabel)
106106

107107
await page.getByTestId('profile-fields-admin-delete').click()
108108

109-
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field deleted successfully.')
109+
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field deleted.')
110110
await expect(page.getByTestId(`profile-fields-admin-definition-${fieldKey}`)).toHaveCount(0)
111111
await deleteDefinitionByFieldKey(page.request, fieldKey)
112112
})
@@ -152,7 +152,7 @@ test('admin uses a modal editor on compact layout', async ({ page }) => {
152152
await createDialog.locator('#profile-fields-admin-label').fill(createdLabel)
153153
await createDialog.getByTestId('profile-fields-admin-save').click()
154154

155-
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created successfully.')
155+
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created.')
156156
await expect(page.getByTestId(`profile-fields-admin-definition-${createdFieldKey}`)).toBeVisible()
157157
await expect(createDialog).toBeHidden()
158158
} finally {
@@ -301,7 +301,7 @@ test('admin gets an initial select option row and can remove empty rows by keybo
301301
await expect(page.getByTestId('profile-fields-admin-option-handle-0')).toBeVisible()
302302

303303
await page.getByTestId('profile-fields-admin-save').click()
304-
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created successfully.')
304+
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created.')
305305

306306
await deleteDefinitionByFieldKey(page.request, fieldKey)
307307
})
@@ -336,7 +336,7 @@ test('admin can bulk add select options from multiple lines', async ({ page }) =
336336
}
337337

338338
await page.getByTestId('profile-fields-admin-save').click()
339-
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created successfully.')
339+
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field created.')
340340

341341
await deleteDefinitionByFieldKey(page.request, fieldKey)
342342
})
@@ -400,7 +400,7 @@ test('admin reuses the empty select option row on repeated Enter', async ({ page
400400
await expect(page.getByTestId('profile-fields-admin-option-row-4')).toHaveCount(0)
401401

402402
await page.getByTestId('profile-fields-admin-save').click()
403-
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated successfully.')
403+
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated.')
404404

405405
await page.reload()
406406
await openSelectDefinitionEditor(page, fieldKey, label)
@@ -451,7 +451,7 @@ test('admin can reorder select options from the handle menu and drag handle', as
451451
await expect(optionInput(page, 3)).toHaveValue('Gamma')
452452

453453
await page.getByTestId('profile-fields-admin-save').click()
454-
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated successfully.')
454+
await expect(page.getByTestId('profile-fields-admin-success')).toContainText('Field updated.')
455455

456456
await page.reload()
457457
await openSelectDefinitionEditor(page, fieldKey, label)

src/components/AdminUserFieldsDialog.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ SPDX-License-Identifier: AGPL-3.0-or-later
123123
<NcButton @click="closeDialog">
124124
{{ t('profile_fields', 'Cancel') }}
125125
</NcButton>
126+
<!-- TRANSLATORS "\u00A0" keeps the ellipsis attached to the previous word for correct typography and avoids awkward line breaks. -->
126127
<NcButton variant="primary" :disabled="!hasPendingChanges || hasInvalidFields || isSavingAny || isLoading" @click="saveAllFields">
127-
{{ isSavingAny ? t('profile_fields', 'Saving changes…') : t('profile_fields', 'Save changes') }}
128+
{{ isSavingAny ? t('profile_fields', 'Saving changes\u00A0…') : t('profile_fields', 'Save changes') }}
128129
</NcButton>
129130
</template>
130131
</NcDialog>
@@ -184,7 +185,8 @@ export default defineComponent({
184185
185186
const headerUserName = computed(() => props.userDisplayName.trim() !== '' ? props.userDisplayName : props.userUid)
186187
const visibilityFieldLabel = t('profile_fields', 'Who can view this field value')
187-
const loadingMessage = computed(() => t('profile_fields', 'Loading profile fields for {userUid}…', { userUid: props.userUid }))
188+
// 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.
189+
const loadingMessage = computed(() => t('profile_fields', 'Loading profile fields for {userUid}\u00A0', { userUid: props.userUid }))
188190
const editableFields = computed<AdminEditableField[]>(() => buildAdminEditableFields(definitions.value, userValues.value))
189191
const isSavingAny = computed(() => savingIds.value.length > 0)
190192
const headerDescription = computed(() => {

src/components/admin/AdminSelectOptionsSection.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,10 @@ const createOptionId = () => `option-local-${nextOptionId++}`
155155
const options = computed(() => props.modelValue)
156156
const bulkOptionValues = computed(() => parseEditableSelectOptionValues(bulkOptionInput.value))
157157
const normalizedOptionCount = computed(() => extractEditableSelectOptionValues(options.value).filter((optionValue: string) => optionValue.trim() !== '').length)
158-
const optionsCountLabel = computed(() => n('profile_fields', 'option', 'options', normalizedOptionCount.value, { count: normalizedOptionCount.value }))
159-
const bulkOptionsSummary = computed(() => n('profile_fields', '1 option ready.', '{count} options ready.', bulkOptionValues.value.length, { count: bulkOptionValues.value.length }))
158+
// TRANSLATORS "Option/Options" here means selectable field values, not application settings.
159+
const optionsCountLabel = computed(() => n('profile_fields', 'Option', 'Options', normalizedOptionCount.value, { count: normalizedOptionCount.value }))
160+
// TRANSLATORS "{count}" is the number of parsed selectable values ready to be added.
161+
const bulkOptionsSummary = computed(() => n('profile_fields', '{count} option ready.', '{count} options ready.', bulkOptionValues.value.length, { count: bulkOptionValues.value.length }))
160162
161163
const duplicateOptionIndices = computed(() => {
162164
const seen = new Map<string, number>()
@@ -184,8 +186,11 @@ const hasOptionValue = (index: number) => options.value[index]?.value.trim() !==
184186
const canMoveOptionUp = (index: number) => index > 0
185187
const canMoveOptionDown = (index: number) => index < options.value.length - 1
186188
const isOptionDuplicate = (index: number) => duplicateOptionIndices.value.has(index)
189+
// TRANSLATORS "{optionValue}" is the visible text of one selectable option.
187190
const reorderOptionLabel = (optionValue: string) => t('profile_fields', 'Reorder option {optionValue}', { optionValue })
191+
// TRANSLATORS "{position}" is a 1-based option index shown as placeholder text.
188192
const optionPlaceholder = (position: number) => t('profile_fields', 'Option {position}', { position })
193+
// TRANSLATORS "{optionValue}" is the visible text of one selectable option.
189194
const removeOptionLabel = (optionValue: string) => t('profile_fields', 'Remove option {optionValue}', { optionValue })
190195
191196
const focusOptionInput = async(index: number) => {

src/tests/components/admin/AdminSelectOptionsSection.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe('AdminSelectOptionsSection', () => {
7676
})
7777

7878
expect(wrapper.text()).toContain('tr:Options')
79-
expect(wrapper.text()).toContain('tr:option')
79+
expect(wrapper.text()).toContain('tr:Option')
8080
expect(wrapper.text()).toContain('tr:Add single option')
8181
})
8282

src/views/AdminSettings.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
149149

150150
<div class="profile-fields-admin__grid profile-fields-admin__grid--identity">
151151
<div class="profile-fields-admin__field">
152+
<!-- TRANSLATORS "Field key" means a stable technical identifier (API key), not a keyboard key. This context applies to both label occurrences below. -->
153+
<!-- TRANSLATORS "APIs" and "integrations" refer to technical systems and external tools. -->
152154
<label for="profile-fields-admin-field-key">{{ t('profile_fields', 'Field key') }}</label>
153155
<NcInputField
154156
id="profile-fields-admin-field-key"
@@ -397,7 +399,8 @@ const editorEmptyState = computed(() => sortedDefinitions.value.length === 0
397399
description: t('profile_fields', 'Select a field from the list, or create a new one.'),
398400
})
399401
const configuredFieldsCountLabel = computed(() => n('profile_fields', 'field configured', 'fields configured', definitions.value.length, { count: definitions.value.length }))
400-
const saveActionLabel = computed(() => isSaving.value ? t('profile_fields', 'Saving changes…') : (isEditing.value ? t('profile_fields', 'Save changes') : t('profile_fields', 'Create field')))
402+
// TRANSLATORS "\u00A0" keeps the ellipsis attached to the previous word for correct typography and avoids awkward line breaks.
403+
const saveActionLabel = computed(() => isSaving.value ? t('profile_fields', 'Saving changes\u00A0') : (isEditing.value ? t('profile_fields', 'Save changes') : t('profile_fields', 'Create field')))
401404
const editFieldAriaLabel = (label: string) => t('profile_fields', 'Edit field {label}', { label })
402405
const actionsForLabel = (label: string) => t('profile_fields', 'Actions for {label}', { label })
403406
const toggleDefinitionActiveLabel = (definition: FieldDefinition) => definition.active
@@ -628,7 +631,7 @@ const persistDefinition = async() => {
628631
selectedId.value = created.id
629632
populateForm(created)
630633
markJustSaved(created.id)
631-
setSuccessMessage(t('profile_fields', 'Field created successfully.'))
634+
setSuccessMessage(t('profile_fields', 'Field created.'))
632635
} else {
633636
const updated = await updateDefinition(selectedDefinition.value.id, {
634637
label: payload.label,
@@ -642,7 +645,7 @@ const persistDefinition = async() => {
642645
replaceDefinitionInState(updated)
643646
populateForm(updated)
644647
markJustSaved(updated.id)
645-
setSuccessMessage(t('profile_fields', 'Field updated successfully.'))
648+
setSuccessMessage(t('profile_fields', 'Field updated.'))
646649
}
647650
if (isCompactLayout.value) {
648651
closeEditor()
@@ -666,7 +669,7 @@ const removeDefinition = async() => {
666669
removeDefinitionFromState(selectedDefinition.value.id)
667670
isCreatingNew.value = false
668671
resetForm()
669-
setSuccessMessage(t('profile_fields', 'Field deleted successfully.'))
672+
setSuccessMessage(t('profile_fields', 'Field deleted.'))
670673
} catch (error: any) {
671674
errorMessage.value = error?.response?.data?.ocs?.data?.message ?? error?.message ?? t('profile_fields', 'Could not delete this field. Please try again.')
672675
} finally {

src/views/PersonalSettings.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
151151
/>
152152

153153
<NcButton variant="primary" :disabled="isSaving(field.definition.id) || !hasFieldChanges(field)" @click="saveField(field)">
154-
{{ isSaving(field.definition.id) ? t('profile_fields', 'Saving changes…') : t('profile_fields', 'Save changes') }}
154+
<!-- TRANSLATORS "\u00A0" keeps the ellipsis attached to the previous word for correct typography and avoids awkward line breaks. -->
155+
{{ isSaving(field.definition.id) ? t('profile_fields', 'Saving changes\u00A0…') : t('profile_fields', 'Save changes') }}
155156
</NcButton>
156157
</div>
157158

src/workflow.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,8 +535,9 @@ class WorkflowProfileFieldElement extends HTMLElement {
535535

536536
const placeholder = document.createElement('option')
537537
placeholder.value = ''
538+
// TRANSLATORS "\u00A0" keeps the ellipsis attached to the previous word for correct typography and avoids awkward line breaks.
538539
placeholder.textContent = definitions.length === 0
539-
? t('profile_fields', 'Loading profile fields…')
540+
? t('profile_fields', 'Loading profile fields\u00A0…')
540541
: t('profile_fields', 'Choose a profile field')
541542
fieldSelect.append(placeholder)
542543

tests/php/Unit/Controller/FieldDefinitionApiControllerTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public function testCreateSelectFieldForwardsOptions(): void {
115115
public function testCreateReturnsBadRequestOnValidationFailure(): void {
116116
$this->service->expects($this->once())
117117
->method('create')
118-
->willThrowException(new InvalidArgumentException('field_key already exists'));
118+
->willThrowException(new InvalidArgumentException('"field_key" already exists'));
119119

120120
$response = $this->controller->create(
121121
'cpf',
@@ -128,7 +128,7 @@ public function testCreateReturnsBadRequestOnValidationFailure(): void {
128128
);
129129

130130
$this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
131-
$this->assertSame(['message' => 'field_key already exists'], $response->getData());
131+
$this->assertSame(['message' => '"field_key" already exists'], $response->getData());
132132
}
133133

134134
public function testUpdateSelectFieldForwardsOptions(): void {

0 commit comments

Comments
 (0)