diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..e7a6803
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: 2026 LibreCode Coop and contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+github: libresign
diff --git a/README.md b/README.md
index a86f394..7539883 100644
--- a/README.md
+++ b/README.md
@@ -3,19 +3,19 @@
# Communications
-**The right message, in the right place, at the right time — inside Nextcloud.**
+**Reach the right people at the right time, directly in Nextcloud.**
Communications is a **free and open-source application for Nextcloud** that helps organizations reach their people without scattering important announcements across unrelated tools. Compose a publication once, choose who should see it and when, then deliver it through the channels that fit the message.
## Communicate where people already work
-- **Contextual banners and dialogs** — show an announcement within Nextcloud, including relevant pages and applications, rather than waiting for people to check another inbox.
-- **A dedicated Inbox** — keep institutional publications accessible after the first notification, with reading and commenting features where enabled.
-- **Nextcloud-native channels** — use Notifications, Activity, and email independently or together, according to your configuration.
-- **Optional messaging channels** — connect supported providers through the separate Two-Factor Gateway app when installed, configured, and allowed by recipient preferences.
-- **Scheduled or event-driven publications** — publish immediately, within a defined period, or in response to registered Nextcloud application events.
-- **Audience and visibility controls** — target users and groups, exclude selected audiences, and define which applications or routes can display a contextual message.
-- **A single publication workspace** — write rich text, preview presentation, manage drafts and active notices, reorder publications, and follow estimated engagement metrics.
+- **Contextual banners and dialogs:** show an announcement within Nextcloud, including relevant pages and applications, rather than waiting for people to check another inbox.
+- **A dedicated Inbox:** keep institutional publications accessible after the first notification, with reading and commenting features where enabled.
+- **Nextcloud-native channels:** use Notifications, Activity, and email independently or together, according to your configuration.
+- **Optional messaging channels:** connect supported providers through the separate Two-Factor Gateway app when installed, configured, and allowed by recipient preferences.
+- **Scheduled or event-driven publications:** publish immediately, within a defined period, or in response to registered Nextcloud application events.
+- **Audience and visibility controls:** target users and groups, exclude selected audiences, and define which applications or routes can display a contextual message.
+- **A single publication workspace:** write rich text, preview presentation, manage drafts and active notices, reorder publications, and follow estimated engagement metrics.
## From everyday notices to automated communication
@@ -37,6 +37,4 @@ If you need help with hosting, deployment, integrations, accessibility, maintena
- [API and integration reference](docs/API.md)
- [Contribute to the project](CONTRIBUTING.md)
-The project grows through code, translations, testing, design, documentation, and financial support. You can help sustain development through [GitHub Sponsors](https://github.com/sponsors/vitormattos) or contact [LibreCode Coop](mailto:contact@librecode.coop) for professional services.
-
-> **Development preview:** Communications is not yet released or certified for production. Current validation uses Nextcloud 36 development builds and disposable test environments. Features and integration contracts may change before the first release.
+The project grows through code, translations, testing, design, documentation, and financial support. You can help sustain development through [GitHub Sponsors](https://github.com/sponsors/libresign) or contact [LibreCode Coop](mailto:contact@librecode.coop) for professional services.
diff --git a/l10n/pt_BR.js b/l10n/pt_BR.js
index a7dfd8d..34eccd2 100644
--- a/l10n/pt_BR.js
+++ b/l10n/pt_BR.js
@@ -454,7 +454,7 @@ OC.L10N.register('communications', {
"Preview message": "Pré-visualizar mensagem",
"Preview presentation": "Visualizar apresentação",
"Previous": "Anterior",
- "Priority (0–100)": "Prioridade (0–100)",
+ "Priority (0 to 100)": "Prioridade (0 a 100)",
"Priority must be between 0 and 100.": "A prioridade deve estar entre 0 e 100.",
"Private bot chat identifier.": "Identificador de chat privado com o bot.",
"Provide a version in the recipient's language. Empty fields use the original text.": "Escreva uma versão para o idioma do usuário. Campos vazios usam o texto original.",
@@ -687,8 +687,9 @@ OC.L10N.register('communications', {
"numeric": "numérico",
"{ready} of {total} providers configured": "{ready} de {total} provedores configurados",
"of": "de",
+ "to": "a",
"options: search by name to find more.": "opções: pesquise pelo nome para encontrar outras.",
"starts with": "começa com",
"{count} in 24 hours": "{count} em 24 horas",
- "— Choose color": "— Escolher cor"
+ "Choose color": "Escolher cor"
}, "nplurals=2; plural=(n != 1);");
diff --git a/l10n/pt_BR.json b/l10n/pt_BR.json
index 7321c89..0bd3ba6 100644
--- a/l10n/pt_BR.json
+++ b/l10n/pt_BR.json
@@ -453,7 +453,7 @@
"Preview message": "Pré-visualizar mensagem",
"Preview presentation": "Visualizar apresentação",
"Previous": "Anterior",
- "Priority (0–100)": "Prioridade (0–100)",
+ "Priority (0 to 100)": "Prioridade (0 a 100)",
"Priority must be between 0 and 100.": "A prioridade deve estar entre 0 e 100.",
"Private bot chat identifier.": "Identificador de chat privado com o bot.",
"Provide a version in the recipient's language. Empty fields use the original text.": "Escreva uma versão para o idioma do usuário. Campos vazios usam o texto original.",
@@ -686,10 +686,11 @@
"numeric": "numérico",
"{ready} of {total} providers configured": "{ready} de {total} provedores configurados",
"of": "de",
+ "to": "a",
"options: search by name to find more.": "opções: pesquise pelo nome para encontrar outras.",
"starts with": "começa com",
"{count} in 24 hours": "{count} em 24 horas",
- "— Choose color": "— Escolher cor"
+ "Choose color": "Escolher cor"
},
"pluralForm": "nplurals=2; plural=(n != 1);"
}
diff --git a/package-lock.json b/package-lock.json
index 107318c..3a843a6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1987,42 +1987,6 @@
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
- "node_modules/@eslint/config-array/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/@eslint/config-array/node_modules/brace-expansion": {
- "version": "5.0.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
- "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/@eslint/config-array/node_modules/minimatch": {
- "version": "10.2.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
- "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^5.0.8"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/@eslint/config-helpers": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
@@ -2509,6 +2473,13 @@
}
}
},
+ "node_modules/@isaacs/nonrecursive-glob-match": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.1.0.tgz",
+ "integrity": "sha512-GPm/Dbvk5RLmhRnxXIa7nTpK0xtl4E0wj0MslsuNiJ1FIROU+2+9lgUC/dgF7Ey4QCWhfEsgxilMRMCdHPfDfw==",
+ "devOptional": true,
+ "license": "BlueOak-1.0.0"
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -3619,48 +3590,12 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/@stryker-mutator/core/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/@stryker-mutator/core/node_modules/brace-expansion": {
- "version": "5.0.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.13.tgz",
- "integrity": "sha512-Vh3dmFbc4wZeox7M9I/0L9GfFd83Su4+yCck/OF9Gi/P/dAHn0RT7X4K9R7gMNMhDck3DvhR6QXsnQdjjgwNvQ==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
"node_modules/@stryker-mutator/core/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
- "node_modules/@stryker-mutator/core/node_modules/minimatch": {
- "version": "10.2.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
- "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^5.0.8"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/@stryker-mutator/instrumenter": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-10.1.1.tgz",
@@ -4012,42 +3947,6 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "5.0.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
- "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "10.2.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
- "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^5.0.8"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.72.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.2.tgz",
@@ -4839,10 +4738,14 @@
}
},
"node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "optional": true
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
},
"node_modules/base-64": {
"version": "1.0.0",
@@ -4882,12 +4785,16 @@
"dev": true
},
"node_modules/brace-expansion": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
- "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
- "optional": true,
+ "version": "5.0.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.13.tgz",
+ "integrity": "sha512-Vh3dmFbc4wZeox7M9I/0L9GfFd83Su4+yCck/OF9Gi/P/dAHn0RT7X4K9R7gMNMhDck3DvhR6QXsnQdjjgwNvQ==",
+ "devOptional": true,
+ "license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0"
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
}
},
"node_modules/braces": {
@@ -5824,27 +5731,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/eslint/node_modules/brace-expansion": {
- "version": "5.0.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
- "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -5857,21 +5743,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/eslint/node_modules/minimatch": {
- "version": "10.2.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
- "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^5.0.8"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/espree": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
@@ -8107,15 +7978,17 @@
"dev": true
},
"node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
- "optional": true,
+ "version": "11.0.8",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.8.tgz",
+ "integrity": "sha512-nFQuYSTVrGvysVBps5Ih+0BdnYFN/Cj3C816r6+p7yK+FHrVLGE4O4N9AFwykrlNlvM2HYt+Rad1Uiro6h88Ig==",
+ "devOptional": true,
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^2.0.2"
+ "@isaacs/nonrecursive-glob-match": "^1.1.0",
+ "brace-expansion": "^5.0.12"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -8912,12 +8785,14 @@
"dev": true
},
"node_modules/qs": {
- "version": "6.15.1",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
- "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "version": "6.17.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.1.tgz",
+ "integrity": "sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==",
"dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
- "side-channel": "^1.1.0"
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
diff --git a/package.json b/package.json
index fc84381..677f6c1 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"test:e2e": "playwright test",
"test:e2e:libresign": "playwright test tests/e2e/libresign-event.spec.mjs",
"lint": "eslint src tests scripts eslint.config.mjs vite*.ts vitest.config.ts playwright.config.mjs && npm run format:vue && npm run format:ts && npm run format:mjs",
- "audit": "npm audit --audit-level=high",
+ "audit": "npm audit --audit-level=moderate",
"test": "npm run test:unit",
"test:coverage": "vitest run --coverage",
"ts:check": "vue-tsc --noEmit",
@@ -70,6 +70,10 @@
"vitest": "4.1.12",
"vue-tsc": "3.3.13"
},
+ "overrides": {
+ "minimatch": "^11.0.1",
+ "qs": "^6.16.1"
+ },
"engines": {
"node": "^24.0.0",
"npm": "^11.0.0"
diff --git a/src/components/CampaignAppearanceFields.vue b/src/components/CampaignAppearanceFields.vue
index 3127101..697c744 100644
--- a/src/components/CampaignAppearanceFields.vue
+++ b/src/components/CampaignAppearanceFields.vue
@@ -93,11 +93,13 @@ const {
backgroundColor: form.banner_color,
}"
aria-hidden="true">
- {{ form.banner_color }}
-
- {{ t('communications', '— Choose color') }}
+
+
+ {{ t('communications', 'Choose color') }}
+ ({{ form.banner_color }})
+
diff --git a/src/components/CampaignEditorReviewStep.vue b/src/components/CampaignEditorReviewStep.vue
index b21ae96..4fd3361 100644
--- a/src/components/CampaignEditorReviewStep.vue
+++ b/src/components/CampaignEditorReviewStep.vue
@@ -111,7 +111,7 @@ const {
diff --git a/src/components/CampaignOverview.vue b/src/components/CampaignOverview.vue
index 38f5d06..9442fc5 100644
--- a/src/components/CampaignOverview.vue
+++ b/src/components/CampaignOverview.vue
@@ -161,7 +161,14 @@ function period(c: CampaignRecord): string {
// TRANSLATORS Publication with neither a start nor an end date restriction.
if (!c.starts_at && !c.ends_at) return t('communications', 'No deadline')
if (c.starts_at && c.ends_at)
- return dateDisplay(c.starts_at) + ' – ' + dateDisplay(c.ends_at)
+ return (
+ // TRANSLATORS Prefix of a date range; preserve the trailing space.
+ t('communications', 'From ')
+ + dateDisplay(c.starts_at)
+ // TRANSLATORS Separator within a date range; preserve the surrounding spaces.
+ + t('communications', ' until ')
+ + dateDisplay(c.ends_at)
+ )
return c.starts_at
? // TRANSLATORS Prefix followed by a dynamic value in the publications management screen; preserve its trailing space.
t('communications', 'Since ') + dateDisplay(c.starts_at)
@@ -525,9 +532,9 @@ const adminActivateActionLabel = t('communications', 'Activate')
{{ t('communications', 'Showing') }}
- {{ (currentPage - 1) * PAGE_SIZE + 1 }}–{{
- Math.min(currentPage * PAGE_SIZE, visible.length)
- }}
+ {{ (currentPage - 1) * PAGE_SIZE + 1 }}
+ {{ t('communications', 'to') }}
+ {{ Math.min(currentPage * PAGE_SIZE, visible.length) }}
{{ t('communications', 'of') }}
{{ visible.length }}
campaigns.value.map((c) => ({
value: String(c.id),
- label: c.name + (c.status === 'active' ? '' : ' — ' + c.status),
+ label: c.name + (c.status === 'active' ? '' : ' (' + c.status + ')'),
})),
)
const reduce = (option: FlowCampaignOption) => option.value
diff --git a/src/components/MarkdownEditor.vue b/src/components/MarkdownEditor.vue
index be096f7..74e130b 100644
--- a/src/components/MarkdownEditor.vue
+++ b/src/components/MarkdownEditor.vue
@@ -291,7 +291,7 @@ const toggleLinePrefix = (view: EditorView, prefix: string) => {
const toLine = state.doc.lineAt(to)
const changes: { from: number; to: number; insert: string }[] = []
- // Detect if ALL selected lines already have the prefix – if so, remove it
+ // Remove the prefix when every selected line already begins with it.
let allHavePrefix = true
for (let lineNo = fromLine.number; lineNo <= toLine.number; lineNo++) {
const line = state.doc.line(lineNo)
diff --git a/src/composables/useCampaignAdminController.ts b/src/composables/useCampaignAdminController.ts
index b2b6ee3..bf683e4 100644
--- a/src/composables/useCampaignAdminController.ts
+++ b/src/composables/useCampaignAdminController.ts
@@ -424,7 +424,7 @@ export function useCampaignAdminController(
// TRANSLATORS Name of a contextual visual delivery format: modal dialog.
const adminModalDialogLabel = t('communications', 'Modal dialog')
// TRANSLATORS Short label for an option or field in the publications management screen.
- const adminPriority0100Label = t('communications', 'Priority (0–100)')
+ const adminPriority0100Label = t('communications', 'Priority (0 to 100)')
// TRANSLATORS Short label for an option or field in the publications management screen.
const adminWhereToDisplayLabel = t('communications', 'Where to display')
// TRANSLATORS Short label for an option or field in the publications management screen.