Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ public function prepare(INotification $notification, string $languageCode): INot
}

if ($notification->getMessage() === 'profile_field_updated_message') {
// TRANSLATORS %1$s is the actor user ID, %2$s is the affected user ID, %3$s is the profile field label.
$notification->setParsedMessage($l10n->t(
'%1$s changed %2$s\'s %3$s profile field.',
'%1$s changed profile field "%3$s" for user %2$s.',
$notification->getMessageParameters(),
));
} elseif ($notification->getMessage() !== '' && $notification->getParsedMessage() === '') {
Expand Down
28 changes: 15 additions & 13 deletions lib/Service/FieldValueService.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function upsert(
$valueJson = $this->encodeValue($normalizedValue);
$visibility = $currentVisibility ?? FieldExposurePolicy::from($definition->getExposurePolicy())->initialVisibility()->value;
if (!FieldVisibility::isValid($visibility)) {
throw new InvalidArgumentException($this->l10n->t('current_visibility is not supported'));
throw new InvalidArgumentException($this->l10n->t('The provided visibility value is not supported.'));
}

$entity = $this->fieldValueMapper->findByFieldDefinitionIdAndUserUid($definition->getId(), $userUid) ?? new FieldValue();
Expand Down Expand Up @@ -145,16 +145,17 @@ public function searchByDefinition(
int $offset,
): 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]));
}

if ($offset < 0) {
throw new InvalidArgumentException($this->l10n->t('offset must be greater than or equal to 0'));
throw new InvalidArgumentException($this->l10n->t('The offset must be greater than or equal to 0.'));
}

$normalizedOperator = strtolower(trim($operator));
if (!in_array($normalizedOperator, [self::SEARCH_OPERATOR_EQ, self::SEARCH_OPERATOR_CONTAINS], true)) {
throw new InvalidArgumentException($this->l10n->t('search operator is not supported'));
throw new InvalidArgumentException($this->l10n->t('The search operator is not supported.'));
}

$searchValue = $this->normalizeSearchValue($definition, $normalizedOperator, $rawValue);
Expand All @@ -177,12 +178,12 @@ public function searchByDefinition(

public function updateVisibility(FieldDefinition $definition, string $userUid, string $updatedByUid, string $currentVisibility): FieldValue {
if (!FieldVisibility::isValid($currentVisibility)) {
throw new InvalidArgumentException($this->l10n->t('current_visibility is not supported'));
throw new InvalidArgumentException($this->l10n->t('The provided visibility value is not supported.'));
}

$entity = $this->fieldValueMapper->findByFieldDefinitionIdAndUserUid($definition->getId(), $userUid);
if ($entity === null) {
throw new InvalidArgumentException($this->l10n->t('field value not found'));
throw new InvalidArgumentException($this->l10n->t('No profile field value was found.'));
}

$previousValue = $this->extractScalarValue($entity->getValueJson());
Expand Down Expand Up @@ -229,7 +230,7 @@ public function serializeForResponse(FieldValue $value): array {
*/
private function normalizeTextValue(array|string|int|float|bool $rawValue): array {
if (is_array($rawValue)) {
throw new InvalidArgumentException($this->l10n->t('text fields expect a scalar value'));
throw new InvalidArgumentException($this->l10n->t('Text fields require a single text value.'));
}

return ['value' => trim((string)$rawValue)];
Expand All @@ -241,12 +242,13 @@ private function normalizeTextValue(array|string|int|float|bool $rawValue): arra
*/
private function normalizeSelectValue(array|string|int|float|bool $rawValue, FieldDefinition $definition): array {
if (!is_string($rawValue)) {
throw new InvalidArgumentException($this->l10n->t('select fields expect a string value'));
throw new InvalidArgumentException($this->l10n->t('Select fields require one of the configured option values.'));
}

$value = trim($rawValue);
$options = json_decode($definition->getOptions() ?? '[]', true);
if (!in_array($value, $options, true)) {
// TRANSLATORS %s is an invalid option value provided by the user.
throw new InvalidArgumentException($this->l10n->t('"%s" is not a valid option for this field', [$value]));
}

Expand All @@ -259,7 +261,7 @@ private function normalizeSelectValue(array|string|int|float|bool $rawValue, Fie
*/
private function normalizeNumberValue(array|string|int|float|bool $rawValue): array {
if (is_array($rawValue) || is_bool($rawValue) || !is_numeric($rawValue)) {
throw new InvalidArgumentException($this->l10n->t('number fields expect a numeric value'));
throw new InvalidArgumentException($this->l10n->t('Number fields require a numeric value.'));
}

return ['value' => str_contains((string)$rawValue, '.') ? (float)$rawValue : (int)$rawValue];
Expand All @@ -272,7 +274,7 @@ private function encodeValue(array $value): string {
try {
return json_encode($value, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw new InvalidArgumentException($this->l10n->t('value_json could not be encoded'), 0, $exception);
throw new InvalidArgumentException($this->l10n->t('The stored value payload could not be encoded as JSON.'), 0, $exception);
}
}

Expand All @@ -283,11 +285,11 @@ private function decodeValue(string $valueJson): array {
try {
$decoded = json_decode($valueJson, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw new InvalidArgumentException($this->l10n->t('value_json could not be decoded'), 0, $exception);
throw new InvalidArgumentException($this->l10n->t('The stored value payload could not be decoded from JSON.'), 0, $exception);
}

if (!is_array($decoded)) {
throw new InvalidArgumentException($this->l10n->t('value_json must decode to an object payload'));
throw new InvalidArgumentException($this->l10n->t('The stored value payload must decode to a JSON object.'));
}

return $decoded;
Expand Down Expand Up @@ -328,13 +330,13 @@ private function normalizeSearchValue(FieldDefinition $definition, string $opera
}

if (FieldType::from($definition->getType()) !== FieldType::TEXT) {
throw new InvalidArgumentException($this->l10n->t('contains operator is only supported for text fields'));
throw new InvalidArgumentException($this->l10n->t('The "contains" operator is only available for text fields.'));
}

$normalized = $this->normalizeValue($definition, $rawValue);
$value = $normalized['value'] ?? null;
if (!is_string($value) || $value === '') {
throw new InvalidArgumentException($this->l10n->t('contains operator requires a non-empty text value'));
throw new InvalidArgumentException($this->l10n->t('The "contains" operator requires a non-empty text value.'));
}

return ['value' => $value];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function isAvailableForScope(int $scope): bool {
#[\Override]
public function validateOperation(string $name, array $checks, string $operation): void {
if (trim($operation) !== '') {
throw new \UnexpectedValueException($this->l10n->t('This workflow operation does not accept custom configuration'));
throw new \UnexpectedValueException($this->l10n->t('This workflow operation does not support custom configuration.'));
}
}

Expand Down Expand Up @@ -90,7 +90,8 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch
}

$this->broker->createConversation(
$this->l10n->t('Profile field change: %1$s for %2$s', [
// TRANSLATORS %1$s is the profile field label, %2$s is the affected user ID.
$this->l10n->t('Profile field changed: %1$s for user %2$s', [
$fieldLabel,
$subject->getUserUid(),
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function isAvailableForScope(int $scope): bool {
public function validateOperation(string $name, array $checks, string $operation): void {
$config = $this->parseConfig($operation);
if ($config === null || $this->resolveRecipientUids($config['targets']) === []) {
throw new \UnexpectedValueException($this->l10n->t('A valid target list is required'));
throw new \UnexpectedValueException($this->l10n->t('A valid recipient list is required.'));
}
}

Expand Down Expand Up @@ -85,8 +85,9 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch

foreach ($this->resolveRecipientUids($config['targets']) as $recipientUid) {
$subjectText = $this->l10n->t('Profile field updated');
// TRANSLATORS %1$s is the actor user ID, %2$s is the affected user ID, %3$s is the profile field label.
$messageText = $this->l10n->t(
'%1$s changed %2$s\'s %3$s profile field.',
'%1$s changed profile field "%3$s" for user %2$s.',
[
$subject->getActorUid(),
$subject->getUserUid(),
Expand Down
13 changes: 8 additions & 5 deletions src/components/AdminSupportBanner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<NcNoteCard v-if="isVisible" type="info" data-testid="profile-fields-admin-support-banner">
<div class="profile-fields-admin-support-banner">
<div class="profile-fields-admin-support-banner__copy">
<p><strong>{{ t('profile_fields', 'Help keep Profile Fields sustainable.') }}</strong></p>
<p><strong>{{ t('profile_fields', 'Help sustain Profile Fields development.') }}</strong></p>
<p>{{ t('profile_fields', 'Profile Fields is open source under the AGPL license and maintained by the LibreCode team, creators of LibreSign.') }}</p>
<p>{{ t('profile_fields', 'If your organization depends on it, please help us sustain its development and maintenance.') }}</p>
<p>{{ t('profile_fields', 'If your organization depends on this app, please help fund ongoing development and maintenance.') }}</p>

<div class="profile-fields-admin-support-banner__actions">
<NcButton class="profile-fields-admin-support-banner__action" variant="primary" @click="openSponsorPage">
Expand All @@ -23,17 +23,17 @@ SPDX-License-Identifier: AGPL-3.0-or-later

<div class="profile-fields-admin-support-banner__links">
<a href="https://github.com/LibreCodeCoop/profile_fields" target="_blank" rel="noopener noreferrer nofollow">
{{ t('profile_fields', 'Give Profile Fields a {star} on GitHub', {star: '⭐'}) }}
{{ githubStarCtaLabel }}
</a>
<a href="mailto:contact@librecode.coop">{{ t('profile_fields', 'Contact us for support or custom development') }}</a>
<a href="mailto:contact@librecode.coop">{{ t('profile_fields', 'Contact us for support or custom development services') }}</a>
</div>
</div>
</div>
</NcNoteCard>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { t } from '@nextcloud/l10n'
import { NcButton, NcNoteCard } from '@nextcloud/vue'

Expand All @@ -47,6 +47,9 @@ const props = withDefaults(defineProps<{

const isVisible = ref(true)

// TRANSLATORS "{star}" is replaced with a star symbol (for example: "⭐").
const githubStarCtaLabel = computed(() => t('profile_fields', 'Star Profile Fields on GitHub {star}', { star: '⭐' }))

const dismissBanner = () => {
isVisible.value = false
try {
Expand Down
9 changes: 8 additions & 1 deletion src/components/AdminUserFieldsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
/>

<div class="profile-fields-user-dialog__visibility-control" :class="{ 'profile-fields-user-dialog__visibility-control--error': fieldHasError(field) }">
<label class="profile-fields-user-dialog__control-label" :for="`profile-fields-user-dialog-visibility-${field.definition.id}`">{{ t('profile_fields', 'Who can see this') }}</label>
<label class="profile-fields-user-dialog__control-label" :for="`profile-fields-user-dialog-visibility-${field.definition.id}`">{{ visibilityFieldLabel }}</label>
<NcSelect
:input-id="`profile-fields-user-dialog-visibility-${field.definition.id}`"
:model-value="visibilityOptionFor(field.definition.id)"
Expand Down Expand Up @@ -156,6 +156,7 @@ export default defineComponent({
const userDraftVisibilities = reactive<Record<number, FieldVisibility>>({})

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 editableFields = computed<AdminEditableField[]>(() => buildAdminEditableFields(definitions.value, userValues.value))
const isSavingAny = computed(() => savingIds.value.length > 0)
Expand All @@ -165,6 +166,7 @@ export default defineComponent({
}

const count = editableFields.value.length
// TRANSLATORS "{count}" is the number of editable fields and "{userUid}" is the account ID (without @).
return n('profile_fields', '{count} editable field for @{userUid}.', '{count} editable fields for @{userUid}.', count, {
count,
userUid: props.userUid,
Expand Down Expand Up @@ -220,12 +222,14 @@ export default defineComponent({
}

if (field.definition.type === 'number' && !plainNumberPattern.test(rawValue)) {
// TRANSLATORS "{fieldLabel}" is a profile field label.
return t('profile_fields', '{fieldLabel} must be a plain numeric value.', { fieldLabel: field.definition.label })
}

if (field.definition.type === 'select') {
const options = field.definition.options ?? []
if (!options.includes(rawValue)) {
// TRANSLATORS "{fieldLabel}" is a profile field label.
return t('profile_fields', '{fieldLabel} must be one of the allowed options.', { fieldLabel: field.definition.label })
}
}
Expand Down Expand Up @@ -311,6 +315,7 @@ export default defineComponent({
'current_visibility is not supported': t('profile_fields', 'The selected visibility is not supported.'),
}[message] ?? (message.includes('is not a valid option')
? t('profile_fields', '{fieldLabel}: invalid option selected.', { fieldLabel: field.definition.label })
// TRANSLATORS "{fieldLabel}" is the field label and "{message}" is a backend error message.
: t('profile_fields', '{fieldLabel}: {message}', { fieldLabel: field.definition.label, message })))
}

Expand Down Expand Up @@ -444,6 +449,7 @@ export default defineComponent({

const hasFieldErrors = changedFields.some((field: AdminEditableField) => Boolean(userValueErrors[field.definition.id]))
if (!hasFieldErrors) {
// TRANSLATORS "{userUid}" is the account ID whose fields were saved.
successMessage.value = t('profile_fields', 'Saved profile fields for {userUid}.', { userUid: props.userUid })
} else {
errorMessage.value = n('profile_fields', 'The field could not be saved.', 'Some fields could not be saved. Review the messages below.', changedFields.length, {
Expand Down Expand Up @@ -475,6 +481,7 @@ export default defineComponent({
headerDescription,
headerUserName,
loadingMessage,
visibilityFieldLabel,
hasPendingChanges,
hasInvalidFields,
helperTextForField,
Expand Down
8 changes: 4 additions & 4 deletions src/tests/components/admin/AdminSupportBanner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@ describe("AdminSupportBanner", () => {
const wrapper = mount(AdminSupportBanner);

expect(wrapper.text()).toContain(
"tr:Help keep Profile Fields sustainable.",
"tr:Help sustain Profile Fields development.",
);
expect(wrapper.text()).toContain(
"tr:Profile Fields is open source under the AGPL license and maintained by the LibreCode team, creators of LibreSign.",
);
expect(wrapper.text()).toContain(
"tr:If your organization depends on it, please help us sustain its development and maintenance.",
"tr:If your organization depends on this app, please help fund ongoing development and maintenance.",
);
expect(wrapper.text()).toContain("tr:Sponsor LibreSign");
expect(wrapper.text()).toContain("tr:Maybe later");
expect(wrapper.text()).toContain("tr:Give Profile Fields a ⭐ on GitHub");
expect(wrapper.text()).toContain("tr:Star Profile Fields on GitHub");
expect(wrapper.text()).toContain(
"tr:Contact us for support or custom development",
"tr:Contact us for support or custom development services",
);
});

Expand Down
Loading
Loading