@asd20/ui-next 2.31.7 → 2.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ # [2.32.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.31.8...ui-next-v2.32.0) (2026-10-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * share handbook translations with multilingual PDF downloads ([a034f23](https://github.com/academydistrict20/asd20-ui-next/commit/a034f237a2021e92376a0ff511f1380286221266))
9
+
10
+ ## [2.31.8](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.31.7...ui-next-v2.31.8) (2026-10-12)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * refine 'send an email' functionality - help text, failure messaging, rate limits, spam detection ([a9c4a28](https://github.com/academydistrict20/asd20-ui-next/commit/a9c4a28f1fdab4c0c551aa2cfb6808fbc29742d2))
16
+
3
17
  ## [2.31.7](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.31.6...ui-next-v2.31.7) (2026-10-12)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.31.7",
3
+ "version": "2.32.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  :validator="validateMessageBody"
42
42
  label="Message"
43
43
  maxlength="8000"
44
- help-text="Please enter at least 5 characters."
44
+ :help-text="messageHelpText"
45
45
  required
46
46
  @validated="validationErrors.messageBody = $event"
47
47
  />
@@ -124,6 +124,8 @@ import Asd20Button from '../../atoms/Asd20Button'
124
124
  import Recaptcha from '../../utils/Recaptcha'
125
125
  import sendEmail from '../../../helpers/sendEmail.js'
126
126
 
127
+ const MIN_MESSAGE_LENGTH = 10
128
+
127
129
  export default {
128
130
  name: 'Asd20ComposeEmailModal',
129
131
  components: {
@@ -161,6 +163,11 @@ export default {
161
163
  },
162
164
  }),
163
165
  computed: {
166
+ messageHelpText() {
167
+ return String(this.emailMessage.messageBody || '').trim().length < MIN_MESSAGE_LENGTH
168
+ ? 'Please provide more detail.'
169
+ : ''
170
+ },
164
171
  modalTitle() {
165
172
  return 'Send an Email'
166
173
  },
@@ -228,8 +235,8 @@ export default {
228
235
  },
229
236
  validateMessageBody({ value, validationErrors }) {
230
237
  const message = String(value || '').trim()
231
- if (message && message.length < 5)
232
- validationErrors.push('The message must be at least 5 characters.')
238
+ if (message && message.length < MIN_MESSAGE_LENGTH)
239
+ validationErrors.push('Please provide more detail.')
233
240
  return validationErrors
234
241
  },
235
242
  getFormValidationMessages() {
@@ -247,8 +254,8 @@ export default {
247
254
  messages.push('Enter a valid email address.')
248
255
  if (!subject) messages.push('A subject is required.')
249
256
  if (!messageBody) messages.push('A message is required.')
250
- else if (messageBody.length < 5)
251
- messages.push('The message must be at least 5 characters.')
257
+ else if (messageBody.length < MIN_MESSAGE_LENGTH)
258
+ messages.push('Please provide more detail.')
252
259
 
253
260
  return messages
254
261
  },
@@ -304,11 +311,25 @@ export default {
304
311
  const statusMatch = String(error?.message || '').match(/\b(\d{3})\b/)
305
312
  return statusMatch ? Number(statusMatch[1]) : null
306
313
  },
314
+ isDeliveryUnconfirmed(error) {
315
+ return this.getSendEmailResponseData(error)?.code === 'delivery_unconfirmed' || this.getSendEmailErrorStatus(error) === null
316
+ },
307
317
  getSendEmailFailureMessage(error) {
318
+ if (this.isDeliveryUnconfirmed(error)) {
319
+ return "We couldn't confirm that your message was sent. Please check with the school before sending it again."
320
+ }
308
321
  if (this.getSendEmailErrorStatus(error) === 429) {
322
+ const response = this.getSendEmailResponseData(error)
323
+ if (response?.code === 'rate_limited' && typeof response.error === 'string' && response.error.trim()) {
324
+ return response.error
325
+ }
309
326
  return 'We could not send your message right now. Please wait and try again later.'
310
327
  }
311
328
 
329
+ const response = this.getSendEmailResponseData(error)
330
+ if (['captcha_failed', 'email_unavailable'].includes(response?.code) && typeof response.error === 'string' && response.error.trim()) {
331
+ return response.error
332
+ }
312
333
  return 'Something went wrong while sending your email. Please try again later.'
313
334
  },
314
335
  getSendEmailResponseData(error) {
@@ -341,7 +362,7 @@ export default {
341
362
  'Enter a valid email address.',
342
363
  'messageBody is required.': 'A message is required.',
343
364
  'messageBody is too short.':
344
- 'The message must be at least 5 characters.',
365
+ 'Please provide more detail.',
345
366
  'messageBody is too long.':
346
367
  'The message must be 8,000 characters or fewer.',
347
368
  'Recipient was not found or does not have a public email address.':
@@ -374,15 +395,18 @@ export default {
374
395
  response?.code === 'moderation_unavailable'
375
396
  ? 'Email temporarily unavailable'
376
397
  : 'Message not sent',
377
- response?.error ||
378
- "We couldn't send your message through this form. For assistance contacting the appropriate staff member, please contact the Academy District 20 Help Desk at help@asd20.org or 719-234-1777.",
398
+ typeof response?.error === 'string' && response.error.trim()
399
+ ? response.error
400
+ : response?.code === 'moderation_unavailable'
401
+ ? "We couldn't send your message right now. Please try again later. If the problem persists, please contact the Academy District 20 Help Desk at help@asd20.org or 719-234-1777."
402
+ : 'Your message could not be sent through this form.',
379
403
  reference
380
404
  )
381
405
  return
382
406
  }
383
407
 
384
408
  this.showFeedback(
385
- 'Email not sent',
409
+ this.isDeliveryUnconfirmed(error) ? 'Delivery not confirmed' : 'Email not sent',
386
410
  this.getSendEmailFailureMessage(error),
387
411
  []
388
412
  )
@@ -2,6 +2,8 @@
2
2
  <div
3
3
  class="asd20-handbook-document"
4
4
  :data-asd20-document-root="documentId"
5
+ :lang="languageCode"
6
+ :dir="languageDirection"
5
7
  >
6
8
  <header class="asd20-handbook-document__cover">
7
9
  <div class="asd20-handbook-document__cover-heading">
@@ -13,10 +15,17 @@
13
15
  v-if="organization.logoImageUrl"
14
16
  class="asd20-handbook-document__logo"
15
17
  :src="organization.logoImageUrl"
16
- :alt="`${coverTitle.organizationTitle} logo`"
18
+ :alt="`${coverTitle.organizationTitle} ${documentLabels.logo || 'logo'}`"
17
19
  />
18
20
  </div>
19
21
  <div class="asd20-handbook-document__cover-body">
22
+ <p
23
+ v-if="languageCode !== 'en'"
24
+ class="asd20-handbook-document__translation-notice"
25
+ >
26
+ {{ documentLabels.automatedTranslation }}
27
+ {{ documentLabels.onlineAuthority }}
28
+ </p>
20
29
  <asd20-messaging
21
30
  :body-content="messages[0]?.bodyContent || ''"
22
31
  :body-json="messages[0]?.bodyJson || ''"
@@ -24,7 +33,10 @@
24
33
  />
25
34
  </div>
26
35
  </header>
27
- <asd20-print-contents :headings="document.headings" />
36
+ <asd20-print-contents
37
+ :headings="document.headings"
38
+ :label="documentLabels.contents || 'Contents'"
39
+ />
28
40
  <main>
29
41
  <article
30
42
  v-for="entry in document.entries"
@@ -75,11 +87,14 @@ export default {
75
87
  printMetadata: { type: Object, default: () => ({}) },
76
88
  includeIntroductionBody: { type: Boolean, default: true },
77
89
  schoolYear: { type: String, default: '' },
90
+ languageCode: { type: String, default: 'en' },
91
+ languageDirection: { type: String, default: 'ltr' },
92
+ documentLabels: { type: Object, default: () => ({}) },
78
93
  },
79
94
  computed: {
80
95
  document() { return buildHandbookDocument(this.$props) },
81
96
  coverTitle() {
82
- return handbookCoverTitle(this.organization, this.document.introduction.heading || this.title, this.printMetadata.generatedAt || new Date().toISOString())
97
+ return handbookCoverTitle(this.organization, this.document.introduction.heading || this.title, this.printMetadata.generatedAt || new Date().toISOString(), this.documentLabels)
83
98
  },
84
99
  },
85
100
  }
@@ -34,6 +34,8 @@
34
34
  :organization-options="organizationOptions"
35
35
  :breadcrumb-links="breadcrumbLinks"
36
36
  :language-code="languageCode"
37
+ :lang="contentLanguageCode || languageCode || 'en'"
38
+ :dir="contentDirection"
37
39
  @update:languageCode="$emit('update:languageCode', $event)"
38
40
  >
39
41
  <template #calls-to-action="{ size }">
@@ -95,7 +97,10 @@
95
97
  <p>
96
98
  Our handbooks are presented in English. You can use the translate button at the top of the page to view the handbook in your preferred language.
97
99
  </p>
98
- <p>
100
+ <p v-if="translatedPdfAvailable">
101
+ When the handbook has finished translating, Download PDF provides a version in that language.
102
+ </p>
103
+ <p v-else>
99
104
  <strong>Coming Soon:</strong> if you are viewing the handbook in a language other than English, and click Download PDF, you'll get a handbook version in that selected language.
100
105
  </p>
101
106
  <p>
@@ -123,6 +128,8 @@
123
128
 
124
129
  <main
125
130
  id="handbook-content"
131
+ :lang="contentLanguageCode || languageCode || 'en'"
132
+ :dir="contentDirection"
126
133
  class="asd20-template-handbook__layout"
127
134
  tabindex="-1"
128
135
  >
@@ -299,6 +306,9 @@ export default {
299
306
  },
300
307
  props: {
301
308
  documentPrint: { type: Boolean, default: false },
309
+ contentLanguageCode: { type: String, default: '' },
310
+ contentDirection: { type: String, default: 'ltr' },
311
+ translatedPdfAvailable: { type: Boolean, default: false },
302
312
  printMetadata: { type: Object, default: () => ({}) },
303
313
  publicationDocumentId: { type: String, default: 'handbook' },
304
314
  publicationDocumentUrl: { type: String, default: '' },
@@ -30,35 +30,38 @@ export function isDistrictHandbook(organization = {}) {
30
30
  )
31
31
  }
32
32
 
33
- function handbookTitles(organization, firstMessageTitle) {
33
+ function handbookTitles(organization, firstMessageTitle, labels = {}) {
34
34
  const organizationTitle = isDistrictHandbook(organization)
35
35
  ? 'Academy District 20'
36
36
  : organization.title || organization.name || 'School'
37
37
  const handbookTitle = isDistrictHandbook(organization)
38
- ? firstMessageTitle || 'School Handbook'
39
- : 'School Handbook'
38
+ ? firstMessageTitle || labels.schoolHandbook || 'School Handbook'
39
+ : labels.schoolHandbook || 'School Handbook'
40
40
  return { organizationTitle, handbookTitle }
41
41
  }
42
42
 
43
43
  export function handbookCoverTitle(
44
44
  organization = {},
45
45
  firstMessageTitle = '',
46
- generatedAt
46
+ generatedAt,
47
+ labels = {}
47
48
  ) {
48
- const { organizationTitle, handbookTitle } = handbookTitles(organization, firstMessageTitle)
49
+ const { organizationTitle, handbookTitle } = handbookTitles(organization, firstMessageTitle, labels)
49
50
  return { organizationTitle, handbookTitle: `${handbookSchoolYear(generatedAt)} ${handbookTitle}` }
50
51
  }
51
52
 
52
- export function handbookFooterTitle(organization = {}, firstMessageTitle = '') {
53
- const { organizationTitle, handbookTitle } = handbookTitles(organization, firstMessageTitle)
53
+ export function handbookFooterTitle(organization = {}, firstMessageTitle = '', labels = {}) {
54
+ const { organizationTitle, handbookTitle } = handbookTitles(organization, firstMessageTitle, labels)
54
55
  return `${organizationTitle} ${handbookTitle}`
55
56
  }
56
57
 
57
58
  export function handbookDownloadFilename(
58
59
  organization = {},
59
60
  generatedAt,
60
- firstMessageTitle = ''
61
+ firstMessageTitle = '',
62
+ language = 'en'
61
63
  ) {
64
+ if (!/^[a-z]{2,3}(?:-[A-Za-z]{2,8})?$/.test(language)) throw new Error('Invalid handbook language.')
62
65
  const safe = (value) =>
63
66
  String(value || '')
64
67
  .replace(/[^a-zA-Z0-9_. ()-]/g, '')
@@ -69,5 +72,5 @@ export function handbookDownloadFilename(
69
72
  const title = isDistrictHandbook(organization)
70
73
  ? `ASD20 ${safe(firstMessageTitle) || 'School Handbook'}`
71
74
  : `${abbreviation ? abbreviation + ' ' : ''}School Handbook`
72
- return `${title} - ${year}-${month}-${day}.pdf`
75
+ return `${title}${language === 'en' ? '' : ` (${language})`} - ${year}-${month}-${day}.pdf`
73
76
  }