@jhb.software/payload-alt-text-plugin 0.11.0 → 0.12.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.
Files changed (40) hide show
  1. package/README.md +82 -39
  2. package/dist/components/BulkGenerateAltTextsButton.js +3 -16
  3. package/dist/components/BulkGenerateAltTextsButton.js.map +1 -1
  4. package/dist/components/summarizeBulkGenerate.d.ts +26 -0
  5. package/dist/components/summarizeBulkGenerate.js +56 -0
  6. package/dist/components/summarizeBulkGenerate.js.map +1 -0
  7. package/dist/endpoints/bulkGenerateAltTexts.d.ts +18 -1
  8. package/dist/endpoints/bulkGenerateAltTexts.js +31 -12
  9. package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
  10. package/dist/endpoints/generateAltText.js +14 -8
  11. package/dist/endpoints/generateAltText.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/plugin.js +1 -0
  15. package/dist/plugin.js.map +1 -1
  16. package/dist/resolvers/anthropic.d.ts +3 -4
  17. package/dist/resolvers/anthropic.js +3 -4
  18. package/dist/resolvers/anthropic.js.map +1 -1
  19. package/dist/resolvers/createVisionResolver.d.ts +1 -1
  20. package/dist/resolvers/createVisionResolver.js.map +1 -1
  21. package/dist/resolvers/mistral.d.ts +4 -3
  22. package/dist/resolvers/mistral.js +4 -3
  23. package/dist/resolvers/mistral.js.map +1 -1
  24. package/dist/resolvers/openAI.d.ts +3 -3
  25. package/dist/resolvers/openAI.js +3 -3
  26. package/dist/resolvers/openAI.js.map +1 -1
  27. package/dist/translations/de.js +12 -4
  28. package/dist/translations/de.js.map +1 -1
  29. package/dist/translations/en.js +12 -4
  30. package/dist/translations/en.js.map +1 -1
  31. package/dist/translations/translation-schema.json +24 -8
  32. package/dist/types/AltTextPluginConfig.d.ts +24 -1
  33. package/dist/types/AltTextPluginConfig.js.map +1 -1
  34. package/dist/utilities/altTextHealth.d.ts +1 -1
  35. package/dist/utilities/altTextHealth.js +29 -4
  36. package/dist/utilities/altTextHealth.js.map +1 -1
  37. package/dist/utilities/resolveLocales.d.ts +15 -0
  38. package/dist/utilities/resolveLocales.js +38 -0
  39. package/dist/utilities/resolveLocales.js.map +1 -0
  40. package/package.json +1 -1
@@ -14,9 +14,15 @@ export const de = {
14
14
  cannotGenerateMissingFields: 'Alternativtext kann nicht generiert werden. Erforderliche Felder fehlen.',
15
15
  errorGeneratingAltText: 'Fehler beim Generieren des Alternativtextes. Bitte versuchen Sie es erneut.',
16
16
  failedToGenerate: 'Generierung des Alternativtextes fehlgeschlagen. Bitte versuchen Sie es erneut.',
17
- failedToGenerateForXImages: 'Generierung des Alternativtextes für {{count}} Bilder fehlgeschlagen.',
17
+ failedToGenerateForXImages_one: 'Generierung des Alternativtextes für {{count}} Bild fehlgeschlagen.',
18
+ failedToGenerateForXImages_other: 'Generierung des Alternativtextes für {{count}} Bilder fehlgeschlagen.',
18
19
  noAltTextGenerated: 'Kein Alternativtext generiert. Bitte versuchen Sie es erneut.',
19
- xOfYImagesUpdated: '{{updated}} von {{total}} Bildern aktualisiert.',
20
+ skippedNoAltTextNeeded_one: '{{count}} Datei übersprungen, die keinen Alternativtext benötigt.',
21
+ skippedNoAltTextNeeded_other: '{{count}} Dateien übersprungen, die keinen Alternativtext benötigen.',
22
+ skippedUnsupportedFormat_one: 'Für {{count}} Datei kann kein Alternativtext generiert werden. Bitte manuell ergänzen.',
23
+ skippedUnsupportedFormat_other: 'Für {{count}} Dateien kann kein Alternativtext generiert werden. Bitte manuell ergänzen.',
24
+ xOfYImagesUpdated_one: '{{updated}} von {{total}} Bild aktualisiert.',
25
+ xOfYImagesUpdated_other: '{{updated}} von {{total}} Bildern aktualisiert.',
20
26
  // Help text
21
27
  altTextDescription: 'Alternativtext für das Bild. Dieser wird für Screenreader und SEO verwendet. Er sollte die folgenden Anforderungen erfüllen:',
22
28
  altTextRequirement1: 'Beschreibt in 1-2 Sätzen, was auf dem Bild zu sehen ist.',
@@ -32,11 +38,13 @@ export const de = {
32
38
  altTextHealthWidget: 'Alternativtexte',
33
39
  collectionCheckFailed: 'Status nicht verfügbar',
34
40
  healthCheckPartialWarning: 'Einige Sammlungen konnten gerade nicht geprüft werden.',
35
- localeCount: '{{count}} Sprachen',
41
+ localeCount_one: '{{count}} Sprache',
42
+ localeCount_other: '{{count}} Sprachen',
36
43
  noImagesFound: 'In den konfigurierten Sammlungen wurden noch keine Bilder gefunden.',
37
44
  statusHealthy: 'Alle vorhanden',
38
45
  statusUnhealthy: '{{count}} fehlend',
39
- totalImageCount: '{{count}} Bilder'
46
+ totalImageCount_one: '{{count}} Bild',
47
+ totalImageCount_other: '{{count}} Bilder'
40
48
  }
41
49
  };
42
50
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/translations/de.ts"],"sourcesContent":["import type { GenericTranslationsObject } from './index.js'\n\nexport const de: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternativtext',\n keywords: 'Schlüsselwörter',\n keywordsDescription:\n 'Schlüsselwörter, die das Bild beschreiben. Wird bei der Suche nach dem Bild verwendet.',\n\n // Button labels\n generateAltText: 'Alternativtext generieren',\n generateAltTextFor_one: 'Alternativtext für {{count}} Bild generieren',\n generateAltTextFor_other: 'Alternativtext für {{count}} Bilder generieren',\n\n // Toast messages\n altTextGeneratedSuccess:\n 'Alternativtext erfolgreich generiert. Bitte überprüfen und speichern Sie das Dokument.',\n cannotGenerateMissingFields:\n 'Alternativtext kann nicht generiert werden. Erforderliche Felder fehlen.',\n errorGeneratingAltText:\n 'Fehler beim Generieren des Alternativtextes. Bitte versuchen Sie es erneut.',\n failedToGenerate:\n 'Generierung des Alternativtextes fehlgeschlagen. Bitte versuchen Sie es erneut.',\n failedToGenerateForXImages:\n 'Generierung des Alternativtextes für {{count}} Bilder fehlgeschlagen.',\n noAltTextGenerated: 'Kein Alternativtext generiert. Bitte versuchen Sie es erneut.',\n xOfYImagesUpdated: '{{updated}} von {{total}} Bildern aktualisiert.',\n\n // Help text\n altTextDescription:\n 'Alternativtext für das Bild. Dieser wird für Screenreader und SEO verwendet. Er sollte die folgenden Anforderungen erfüllen:',\n altTextRequirement1: 'Beschreibt in 1-2 Sätzen, was auf dem Bild zu sehen ist.',\n altTextRequirement2:\n 'Sollte möglichst die gleichen Informationen oder den gleichen Zweck wie das Bild vermitteln.',\n altTextRequirement3:\n 'Phrasen wie \"Bild von\" oder \"Foto von\" sind überflüssig, da Screenreader bereits anzeigen, dass es sich um ein Bild handelt.',\n\n // Tooltips\n pleaseSaveDocumentFirst: 'Bitte speichern Sie zuerst das Dokument',\n unsupportedMimeType:\n 'Alternativtext-Generierung wird für {{mimeType}}-Dateien nicht unterstützt',\n\n // Validation messages\n theAlternateTextIsRequired: 'Der Alternativtext ist erforderlich.',\n\n // Dashboard widget\n altTextHealthDescription: 'Alternativtext Abdeckung in allen Upload-Sammlungen.',\n altTextHealthWidget: 'Alternativtexte',\n collectionCheckFailed: 'Status nicht verfügbar',\n healthCheckPartialWarning: 'Einige Sammlungen konnten gerade nicht geprüft werden.',\n localeCount: '{{count}} Sprachen',\n noImagesFound: 'In den konfigurierten Sammlungen wurden noch keine Bilder gefunden.',\n statusHealthy: 'Alle vorhanden',\n statusUnhealthy: '{{count}} fehlend',\n totalImageCount: '{{count}} Bilder',\n },\n}\n"],"names":["de","$schema","alternateText","keywords","keywordsDescription","generateAltText","generateAltTextFor_one","generateAltTextFor_other","altTextGeneratedSuccess","cannotGenerateMissingFields","errorGeneratingAltText","failedToGenerate","failedToGenerateForXImages","noAltTextGenerated","xOfYImagesUpdated","altTextDescription","altTextRequirement1","altTextRequirement2","altTextRequirement3","pleaseSaveDocumentFirst","unsupportedMimeType","theAlternateTextIsRequired","altTextHealthDescription","altTextHealthWidget","collectionCheckFailed","healthCheckPartialWarning","localeCount","noImagesFound","statusHealthy","statusUnhealthy","totalImageCount"],"mappings":"AAEA,OAAO,MAAMA,KAAgC;IAC3CC,SAAS;IACT,yCAAyC;QACvC,eAAe;QACfC,eAAe;QACfC,UAAU;QACVC,qBACE;QAEF,gBAAgB;QAChBC,iBAAiB;QACjBC,wBAAwB;QACxBC,0BAA0B;QAE1B,iBAAiB;QACjBC,yBACE;QACFC,6BACE;QACFC,wBACE;QACFC,kBACE;QACFC,4BACE;QACFC,oBAAoB;QACpBC,mBAAmB;QAEnB,YAAY;QACZC,oBACE;QACFC,qBAAqB;QACrBC,qBACE;QACFC,qBACE;QAEF,WAAW;QACXC,yBAAyB;QACzBC,qBACE;QAEF,sBAAsB;QACtBC,4BAA4B;QAE5B,mBAAmB;QACnBC,0BAA0B;QAC1BC,qBAAqB;QACrBC,uBAAuB;QACvBC,2BAA2B;QAC3BC,aAAa;QACbC,eAAe;QACfC,eAAe;QACfC,iBAAiB;QACjBC,iBAAiB;IACnB;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/translations/de.ts"],"sourcesContent":["import type { GenericTranslationsObject } from './index.js'\n\nexport const de: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternativtext',\n keywords: 'Schlüsselwörter',\n keywordsDescription:\n 'Schlüsselwörter, die das Bild beschreiben. Wird bei der Suche nach dem Bild verwendet.',\n\n // Button labels\n generateAltText: 'Alternativtext generieren',\n generateAltTextFor_one: 'Alternativtext für {{count}} Bild generieren',\n generateAltTextFor_other: 'Alternativtext für {{count}} Bilder generieren',\n\n // Toast messages\n altTextGeneratedSuccess:\n 'Alternativtext erfolgreich generiert. Bitte überprüfen und speichern Sie das Dokument.',\n cannotGenerateMissingFields:\n 'Alternativtext kann nicht generiert werden. Erforderliche Felder fehlen.',\n errorGeneratingAltText:\n 'Fehler beim Generieren des Alternativtextes. Bitte versuchen Sie es erneut.',\n failedToGenerate:\n 'Generierung des Alternativtextes fehlgeschlagen. Bitte versuchen Sie es erneut.',\n failedToGenerateForXImages_one:\n 'Generierung des Alternativtextes für {{count}} Bild fehlgeschlagen.',\n failedToGenerateForXImages_other:\n 'Generierung des Alternativtextes für {{count}} Bilder fehlgeschlagen.',\n noAltTextGenerated: 'Kein Alternativtext generiert. Bitte versuchen Sie es erneut.',\n skippedNoAltTextNeeded_one: '{{count}} Datei übersprungen, die keinen Alternativtext benötigt.',\n skippedNoAltTextNeeded_other:\n '{{count}} Dateien übersprungen, die keinen Alternativtext benötigen.',\n skippedUnsupportedFormat_one:\n 'Für {{count}} Datei kann kein Alternativtext generiert werden. Bitte manuell ergänzen.',\n skippedUnsupportedFormat_other:\n 'Für {{count}} Dateien kann kein Alternativtext generiert werden. Bitte manuell ergänzen.',\n xOfYImagesUpdated_one: '{{updated}} von {{total}} Bild aktualisiert.',\n xOfYImagesUpdated_other: '{{updated}} von {{total}} Bildern aktualisiert.',\n\n // Help text\n altTextDescription:\n 'Alternativtext für das Bild. Dieser wird für Screenreader und SEO verwendet. Er sollte die folgenden Anforderungen erfüllen:',\n altTextRequirement1: 'Beschreibt in 1-2 Sätzen, was auf dem Bild zu sehen ist.',\n altTextRequirement2:\n 'Sollte möglichst die gleichen Informationen oder den gleichen Zweck wie das Bild vermitteln.',\n altTextRequirement3:\n 'Phrasen wie \"Bild von\" oder \"Foto von\" sind überflüssig, da Screenreader bereits anzeigen, dass es sich um ein Bild handelt.',\n\n // Tooltips\n pleaseSaveDocumentFirst: 'Bitte speichern Sie zuerst das Dokument',\n unsupportedMimeType:\n 'Alternativtext-Generierung wird für {{mimeType}}-Dateien nicht unterstützt',\n\n // Validation messages\n theAlternateTextIsRequired: 'Der Alternativtext ist erforderlich.',\n\n // Dashboard widget\n altTextHealthDescription: 'Alternativtext Abdeckung in allen Upload-Sammlungen.',\n altTextHealthWidget: 'Alternativtexte',\n collectionCheckFailed: 'Status nicht verfügbar',\n healthCheckPartialWarning: 'Einige Sammlungen konnten gerade nicht geprüft werden.',\n localeCount_one: '{{count}} Sprache',\n localeCount_other: '{{count}} Sprachen',\n noImagesFound: 'In den konfigurierten Sammlungen wurden noch keine Bilder gefunden.',\n statusHealthy: 'Alle vorhanden',\n statusUnhealthy: '{{count}} fehlend',\n totalImageCount_one: '{{count}} Bild',\n totalImageCount_other: '{{count}} Bilder',\n },\n}\n"],"names":["de","$schema","alternateText","keywords","keywordsDescription","generateAltText","generateAltTextFor_one","generateAltTextFor_other","altTextGeneratedSuccess","cannotGenerateMissingFields","errorGeneratingAltText","failedToGenerate","failedToGenerateForXImages_one","failedToGenerateForXImages_other","noAltTextGenerated","skippedNoAltTextNeeded_one","skippedNoAltTextNeeded_other","skippedUnsupportedFormat_one","skippedUnsupportedFormat_other","xOfYImagesUpdated_one","xOfYImagesUpdated_other","altTextDescription","altTextRequirement1","altTextRequirement2","altTextRequirement3","pleaseSaveDocumentFirst","unsupportedMimeType","theAlternateTextIsRequired","altTextHealthDescription","altTextHealthWidget","collectionCheckFailed","healthCheckPartialWarning","localeCount_one","localeCount_other","noImagesFound","statusHealthy","statusUnhealthy","totalImageCount_one","totalImageCount_other"],"mappings":"AAEA,OAAO,MAAMA,KAAgC;IAC3CC,SAAS;IACT,yCAAyC;QACvC,eAAe;QACfC,eAAe;QACfC,UAAU;QACVC,qBACE;QAEF,gBAAgB;QAChBC,iBAAiB;QACjBC,wBAAwB;QACxBC,0BAA0B;QAE1B,iBAAiB;QACjBC,yBACE;QACFC,6BACE;QACFC,wBACE;QACFC,kBACE;QACFC,gCACE;QACFC,kCACE;QACFC,oBAAoB;QACpBC,4BAA4B;QAC5BC,8BACE;QACFC,8BACE;QACFC,gCACE;QACFC,uBAAuB;QACvBC,yBAAyB;QAEzB,YAAY;QACZC,oBACE;QACFC,qBAAqB;QACrBC,qBACE;QACFC,qBACE;QAEF,WAAW;QACXC,yBAAyB;QACzBC,qBACE;QAEF,sBAAsB;QACtBC,4BAA4B;QAE5B,mBAAmB;QACnBC,0BAA0B;QAC1BC,qBAAqB;QACrBC,uBAAuB;QACvBC,2BAA2B;QAC3BC,iBAAiB;QACjBC,mBAAmB;QACnBC,eAAe;QACfC,eAAe;QACfC,iBAAiB;QACjBC,qBAAqB;QACrBC,uBAAuB;IACzB;AACF,EAAC"}
@@ -14,9 +14,15 @@ export const en = {
14
14
  cannotGenerateMissingFields: 'Cannot generate alt text. Missing required fields.',
15
15
  errorGeneratingAltText: 'Error generating alt text. Please try again.',
16
16
  failedToGenerate: 'Failed to generate alt text. Please try again.',
17
- failedToGenerateForXImages: 'Failed to generate alt text for {{count}} images.',
17
+ failedToGenerateForXImages_one: 'Failed to generate alt text for {{count}} image.',
18
+ failedToGenerateForXImages_other: 'Failed to generate alt text for {{count}} images.',
18
19
  noAltTextGenerated: 'No alt text generated. Please try again.',
19
- xOfYImagesUpdated: '{{updated}} of {{total}} images updated.',
20
+ skippedNoAltTextNeeded_one: 'Skipped {{count}} file that does not need alt text.',
21
+ skippedNoAltTextNeeded_other: 'Skipped {{count}} files that do not need alt text.',
22
+ skippedUnsupportedFormat_one: 'Alt text cannot be generated for {{count}} file. Please write it by hand.',
23
+ skippedUnsupportedFormat_other: 'Alt text cannot be generated for {{count}} files. Please write it by hand.',
24
+ xOfYImagesUpdated_one: '{{updated}} of {{total}} image updated.',
25
+ xOfYImagesUpdated_other: '{{updated}} of {{total}} images updated.',
20
26
  // Help text
21
27
  altTextDescription: 'Alternate text for the image. This will be used for screen readers and SEO. It should meet the following requirements:',
22
28
  altTextRequirement1: 'Describes in 1-2 sentences, what is visible in the image.',
@@ -32,11 +38,13 @@ export const en = {
32
38
  altTextHealthWidget: 'Alt Texts',
33
39
  collectionCheckFailed: 'Status unavailable',
34
40
  healthCheckPartialWarning: 'Some collections could not be checked right now.',
35
- localeCount: '{{count}} locales',
41
+ localeCount_one: '{{count}} locale',
42
+ localeCount_other: '{{count}} locales',
36
43
  noImagesFound: 'No images found in the configured collections yet.',
37
44
  statusHealthy: 'All set',
38
45
  statusUnhealthy: '{{count}} missing',
39
- totalImageCount: '{{count}} images'
46
+ totalImageCount_one: '{{count}} image',
47
+ totalImageCount_other: '{{count}} images'
40
48
  }
41
49
  };
42
50
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/translations/en.ts"],"sourcesContent":["import type { GenericTranslationsObject } from './index.js'\n\nexport const en: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternate text',\n keywords: 'Keywords',\n keywordsDescription: 'Keywords which describe the image. Used when searching for the image.',\n\n // Button labels\n generateAltText: 'Generate alt text',\n generateAltTextFor_one: 'Generate alt text for {{count}} image',\n generateAltTextFor_other: 'Generate alt text for {{count}} images',\n\n // Toast messages\n altTextGeneratedSuccess:\n 'Alt text generated successfully. Please review and save the document.',\n cannotGenerateMissingFields: 'Cannot generate alt text. Missing required fields.',\n errorGeneratingAltText: 'Error generating alt text. Please try again.',\n failedToGenerate: 'Failed to generate alt text. Please try again.',\n failedToGenerateForXImages: 'Failed to generate alt text for {{count}} images.',\n noAltTextGenerated: 'No alt text generated. Please try again.',\n xOfYImagesUpdated: '{{updated}} of {{total}} images updated.',\n\n // Help text\n altTextDescription:\n 'Alternate text for the image. This will be used for screen readers and SEO. It should meet the following requirements:',\n altTextRequirement1: 'Describes in 1-2 sentences, what is visible in the image.',\n altTextRequirement2:\n 'Should convey the same information or purpose as the image, whenever possible.',\n altTextRequirement3:\n 'Phrases like \"image of\" or \"picture of\" are unnecessary, since screen readers already announce that it\\'s an image.',\n\n // Tooltips\n pleaseSaveDocumentFirst: 'Please save the document first',\n unsupportedMimeType: 'Alt text generation is not supported for {{mimeType}} files',\n\n // Validation messages\n theAlternateTextIsRequired: 'An alternate text is required.',\n\n // Dashboard widget\n altTextHealthDescription: 'Alt text coverage across upload collections.',\n altTextHealthWidget: 'Alt Texts',\n collectionCheckFailed: 'Status unavailable',\n healthCheckPartialWarning: 'Some collections could not be checked right now.',\n localeCount: '{{count}} locales',\n noImagesFound: 'No images found in the configured collections yet.',\n statusHealthy: 'All set',\n statusUnhealthy: '{{count}} missing',\n totalImageCount: '{{count}} images',\n },\n}\n"],"names":["en","$schema","alternateText","keywords","keywordsDescription","generateAltText","generateAltTextFor_one","generateAltTextFor_other","altTextGeneratedSuccess","cannotGenerateMissingFields","errorGeneratingAltText","failedToGenerate","failedToGenerateForXImages","noAltTextGenerated","xOfYImagesUpdated","altTextDescription","altTextRequirement1","altTextRequirement2","altTextRequirement3","pleaseSaveDocumentFirst","unsupportedMimeType","theAlternateTextIsRequired","altTextHealthDescription","altTextHealthWidget","collectionCheckFailed","healthCheckPartialWarning","localeCount","noImagesFound","statusHealthy","statusUnhealthy","totalImageCount"],"mappings":"AAEA,OAAO,MAAMA,KAAgC;IAC3CC,SAAS;IACT,yCAAyC;QACvC,eAAe;QACfC,eAAe;QACfC,UAAU;QACVC,qBAAqB;QAErB,gBAAgB;QAChBC,iBAAiB;QACjBC,wBAAwB;QACxBC,0BAA0B;QAE1B,iBAAiB;QACjBC,yBACE;QACFC,6BAA6B;QAC7BC,wBAAwB;QACxBC,kBAAkB;QAClBC,4BAA4B;QAC5BC,oBAAoB;QACpBC,mBAAmB;QAEnB,YAAY;QACZC,oBACE;QACFC,qBAAqB;QACrBC,qBACE;QACFC,qBACE;QAEF,WAAW;QACXC,yBAAyB;QACzBC,qBAAqB;QAErB,sBAAsB;QACtBC,4BAA4B;QAE5B,mBAAmB;QACnBC,0BAA0B;QAC1BC,qBAAqB;QACrBC,uBAAuB;QACvBC,2BAA2B;QAC3BC,aAAa;QACbC,eAAe;QACfC,eAAe;QACfC,iBAAiB;QACjBC,iBAAiB;IACnB;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/translations/en.ts"],"sourcesContent":["import type { GenericTranslationsObject } from './index.js'\n\nexport const en: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternate text',\n keywords: 'Keywords',\n keywordsDescription: 'Keywords which describe the image. Used when searching for the image.',\n\n // Button labels\n generateAltText: 'Generate alt text',\n generateAltTextFor_one: 'Generate alt text for {{count}} image',\n generateAltTextFor_other: 'Generate alt text for {{count}} images',\n\n // Toast messages\n altTextGeneratedSuccess:\n 'Alt text generated successfully. Please review and save the document.',\n cannotGenerateMissingFields: 'Cannot generate alt text. Missing required fields.',\n errorGeneratingAltText: 'Error generating alt text. Please try again.',\n failedToGenerate: 'Failed to generate alt text. Please try again.',\n failedToGenerateForXImages_one: 'Failed to generate alt text for {{count}} image.',\n failedToGenerateForXImages_other: 'Failed to generate alt text for {{count}} images.',\n noAltTextGenerated: 'No alt text generated. Please try again.',\n skippedNoAltTextNeeded_one: 'Skipped {{count}} file that does not need alt text.',\n skippedNoAltTextNeeded_other: 'Skipped {{count}} files that do not need alt text.',\n skippedUnsupportedFormat_one:\n 'Alt text cannot be generated for {{count}} file. Please write it by hand.',\n skippedUnsupportedFormat_other:\n 'Alt text cannot be generated for {{count}} files. Please write it by hand.',\n xOfYImagesUpdated_one: '{{updated}} of {{total}} image updated.',\n xOfYImagesUpdated_other: '{{updated}} of {{total}} images updated.',\n\n // Help text\n altTextDescription:\n 'Alternate text for the image. This will be used for screen readers and SEO. It should meet the following requirements:',\n altTextRequirement1: 'Describes in 1-2 sentences, what is visible in the image.',\n altTextRequirement2:\n 'Should convey the same information or purpose as the image, whenever possible.',\n altTextRequirement3:\n 'Phrases like \"image of\" or \"picture of\" are unnecessary, since screen readers already announce that it\\'s an image.',\n\n // Tooltips\n pleaseSaveDocumentFirst: 'Please save the document first',\n unsupportedMimeType: 'Alt text generation is not supported for {{mimeType}} files',\n\n // Validation messages\n theAlternateTextIsRequired: 'An alternate text is required.',\n\n // Dashboard widget\n altTextHealthDescription: 'Alt text coverage across upload collections.',\n altTextHealthWidget: 'Alt Texts',\n collectionCheckFailed: 'Status unavailable',\n healthCheckPartialWarning: 'Some collections could not be checked right now.',\n localeCount_one: '{{count}} locale',\n localeCount_other: '{{count}} locales',\n noImagesFound: 'No images found in the configured collections yet.',\n statusHealthy: 'All set',\n statusUnhealthy: '{{count}} missing',\n totalImageCount_one: '{{count}} image',\n totalImageCount_other: '{{count}} images',\n },\n}\n"],"names":["en","$schema","alternateText","keywords","keywordsDescription","generateAltText","generateAltTextFor_one","generateAltTextFor_other","altTextGeneratedSuccess","cannotGenerateMissingFields","errorGeneratingAltText","failedToGenerate","failedToGenerateForXImages_one","failedToGenerateForXImages_other","noAltTextGenerated","skippedNoAltTextNeeded_one","skippedNoAltTextNeeded_other","skippedUnsupportedFormat_one","skippedUnsupportedFormat_other","xOfYImagesUpdated_one","xOfYImagesUpdated_other","altTextDescription","altTextRequirement1","altTextRequirement2","altTextRequirement3","pleaseSaveDocumentFirst","unsupportedMimeType","theAlternateTextIsRequired","altTextHealthDescription","altTextHealthWidget","collectionCheckFailed","healthCheckPartialWarning","localeCount_one","localeCount_other","noImagesFound","statusHealthy","statusUnhealthy","totalImageCount_one","totalImageCount_other"],"mappings":"AAEA,OAAO,MAAMA,KAAgC;IAC3CC,SAAS;IACT,yCAAyC;QACvC,eAAe;QACfC,eAAe;QACfC,UAAU;QACVC,qBAAqB;QAErB,gBAAgB;QAChBC,iBAAiB;QACjBC,wBAAwB;QACxBC,0BAA0B;QAE1B,iBAAiB;QACjBC,yBACE;QACFC,6BAA6B;QAC7BC,wBAAwB;QACxBC,kBAAkB;QAClBC,gCAAgC;QAChCC,kCAAkC;QAClCC,oBAAoB;QACpBC,4BAA4B;QAC5BC,8BAA8B;QAC9BC,8BACE;QACFC,gCACE;QACFC,uBAAuB;QACvBC,yBAAyB;QAEzB,YAAY;QACZC,oBACE;QACFC,qBAAqB;QACrBC,qBACE;QACFC,qBACE;QAEF,WAAW;QACXC,yBAAyB;QACzBC,qBAAqB;QAErB,sBAAsB;QACtBC,4BAA4B;QAE5B,mBAAmB;QACnBC,0BAA0B;QAC1BC,qBAAqB;QACrBC,uBAAuB;QACvBC,2BAA2B;QAC3BC,iBAAiB;QACjBC,mBAAmB;QACnBC,eAAe;QACfC,eAAe;QACfC,iBAAiB;QACjBC,qBAAqB;QACrBC,uBAAuB;IACzB;AACF,EAAC"}
@@ -17,22 +17,30 @@
17
17
  "collectionCheckFailed": { "type": "string" },
18
18
  "errorGeneratingAltText": { "type": "string" },
19
19
  "failedToGenerate": { "type": "string" },
20
- "failedToGenerateForXImages": { "type": "string" },
20
+ "failedToGenerateForXImages_one": { "type": "string" },
21
+ "failedToGenerateForXImages_other": { "type": "string" },
21
22
  "generateAltText": { "type": "string" },
22
23
  "generateAltTextFor_one": { "type": "string" },
23
24
  "generateAltTextFor_other": { "type": "string" },
24
25
  "healthCheckPartialWarning": { "type": "string" },
25
26
  "keywords": { "type": "string" },
26
- "localeCount": { "type": "string" },
27
+ "localeCount_one": { "type": "string" },
28
+ "localeCount_other": { "type": "string" },
27
29
  "keywordsDescription": { "type": "string" },
28
30
  "noAltTextGenerated": { "type": "string" },
29
31
  "noImagesFound": { "type": "string" },
30
32
  "pleaseSaveDocumentFirst": { "type": "string" },
33
+ "skippedNoAltTextNeeded_one": { "type": "string" },
34
+ "skippedNoAltTextNeeded_other": { "type": "string" },
35
+ "skippedUnsupportedFormat_one": { "type": "string" },
36
+ "skippedUnsupportedFormat_other": { "type": "string" },
31
37
  "statusHealthy": { "type": "string" },
32
38
  "statusUnhealthy": { "type": "string" },
33
39
  "theAlternateTextIsRequired": { "type": "string" },
34
- "totalImageCount": { "type": "string" },
35
- "xOfYImagesUpdated": { "type": "string" }
40
+ "totalImageCount_one": { "type": "string" },
41
+ "totalImageCount_other": { "type": "string" },
42
+ "xOfYImagesUpdated_one": { "type": "string" },
43
+ "xOfYImagesUpdated_other": { "type": "string" }
36
44
  },
37
45
  "required": [
38
46
  "alternateText",
@@ -47,22 +55,30 @@
47
55
  "collectionCheckFailed",
48
56
  "errorGeneratingAltText",
49
57
  "failedToGenerate",
50
- "failedToGenerateForXImages",
58
+ "failedToGenerateForXImages_one",
59
+ "failedToGenerateForXImages_other",
51
60
  "generateAltText",
52
61
  "generateAltTextFor_one",
53
62
  "generateAltTextFor_other",
54
63
  "healthCheckPartialWarning",
55
64
  "keywords",
56
65
  "keywordsDescription",
57
- "localeCount",
66
+ "localeCount_one",
67
+ "localeCount_other",
58
68
  "noAltTextGenerated",
59
69
  "noImagesFound",
60
70
  "pleaseSaveDocumentFirst",
71
+ "skippedNoAltTextNeeded_one",
72
+ "skippedNoAltTextNeeded_other",
73
+ "skippedUnsupportedFormat_one",
74
+ "skippedUnsupportedFormat_other",
61
75
  "statusHealthy",
62
76
  "statusUnhealthy",
63
77
  "theAlternateTextIsRequired",
64
- "totalImageCount",
65
- "xOfYImagesUpdated"
78
+ "totalImageCount_one",
79
+ "totalImageCount_other",
80
+ "xOfYImagesUpdated_one",
81
+ "xOfYImagesUpdated_other"
66
82
  ]
67
83
  }
68
84
  },
@@ -36,6 +36,20 @@ export type AltTextHealthBaseFilter = (args: {
36
36
  collection: CollectionSlug;
37
37
  req: PayloadRequest;
38
38
  }) => Promise<Where> | Where;
39
+ /**
40
+ * Narrows the locales a request generates for and is measured against — in a
41
+ * multi-tenant CMS, to the locales of the tenant the request is for.
42
+ *
43
+ * Must return a non-empty subset of `locales`; anything else ends the operation
44
+ * with an error rather than writing into a locale the project does not define.
45
+ *
46
+ * @param args.locales The locales configured on the Payload config.
47
+ * @param args.req The request being served.
48
+ */
49
+ export type FilterLocales = (args: {
50
+ locales: string[];
51
+ req: PayloadRequest;
52
+ }) => Promise<string[]> | string[];
39
53
  /** Configuration of the alt text health feature. */
40
54
  export type AltTextHealthCheckConfig = {
41
55
  /**
@@ -92,6 +106,13 @@ export type IncomingAltTextPluginConfig = {
92
106
  fieldsOverride?: (args: {
93
107
  defaultFields: Field[];
94
108
  }) => Field[];
109
+ /**
110
+ * Narrows the locales this request targets. See {@link FilterLocales}.
111
+ *
112
+ * Governs bulk generation, the generate endpoint's locale validation, and the
113
+ * health report.
114
+ */
115
+ filterLocales?: FilterLocales;
95
116
  /**
96
117
  * Builds the image URL sent to the resolver. See {@link GetImageThumbnail}.
97
118
  *
@@ -167,6 +188,8 @@ export type AltTextPluginConfig = {
167
188
  fieldsOverride?: (args: {
168
189
  defaultFields: Field[];
169
190
  }) => Field[];
191
+ /** Narrows the locales one request targets. See {@link FilterLocales}. */
192
+ filterLocales?: FilterLocales;
170
193
  /** Function to get the thumbnail URL of an image document. */
171
194
  getImageThumbnail: GetImageThumbnail;
172
195
  /** Whether alt text health tracking is enabled. */
@@ -179,7 +202,7 @@ export type AltTextPluginConfig = {
179
202
  healthCheckBaseFilter?: AltTextHealthBaseFilter;
180
203
  /** The locale to generate alt texts in when localization is disabled. */
181
204
  locale?: string;
182
- /** The locales to generate alt texts for. */
205
+ /** The locales configured on the Payload config. Empty when localization is disabled. */
183
206
  locales: string[];
184
207
  /** Maximum number of concurrent API requests for bulk generate operations. */
185
208
  maxBulkGenerateConcurrency: number;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types/AltTextPluginConfig.ts"],"sourcesContent":["import type { CollectionSlug, Field, PayloadRequest, Where } from 'payload'\n\nimport type { AltTextResolver } from '../resolvers/types.js'\nimport type {\n AltTextCollectionConfig,\n IncomingCollectionsConfig,\n NormalizedAltTextCollectionConfig,\n} from '../utilities/mimeTypes.js'\n\nexport type { AltTextCollectionConfig, NormalizedAltTextCollectionConfig }\n\n/**\n * Builds the thumbnail URL the resolver fetches. Must be publicly reachable.\n *\n * May be async, so the URL can be signed on demand (S3 presigning, short-lived\n * CDN tokens).\n *\n * @param doc The upload document to build the URL for.\n * @param args.collection The slug of the collection `doc` belongs to — use it to\n * build different URLs per collection (e.g. a Cloudinary transformation for one\n * collection and a plain S3 URL for another).\n * @param args.req The request the generation runs under.\n */\nexport type GetImageThumbnail = (\n doc: Record<string, unknown>,\n args: { collection: CollectionSlug; req: PayloadRequest },\n) => Promise<string> | string\n\n/**\n * Narrows the health scan of one collection to a subset of its documents — in a\n * multi-tenant CMS, to the tenant the request is for. The returned constraint is\n * ANDed onto the scan's MIME type filter.\n *\n * Called once per configured collection, so collections that do not carry the\n * constraining field (a media library shared across tenants, say) can return `{}`\n * instead of being queried for a field they do not have.\n *\n * Return `{}` to scan the collection whole.\n *\n * @param args.collection The slug of the collection being scanned.\n * @param args.req The request the scan runs for.\n */\nexport type AltTextHealthBaseFilter = (args: {\n collection: CollectionSlug\n req: PayloadRequest\n}) => Promise<Where> | Where\n\n/** Configuration of the alt text health feature. */\nexport type AltTextHealthCheckConfig = {\n /**\n * Access control for the health report — both the REST endpoint and the dashboard\n * widget, which hides itself when this denies.\n *\n * Use it to restrict the collection-wide report more strictly than the\n * per-document generate endpoints (e.g. to admins).\n *\n * @default the plugin's `access`\n */\n access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Narrows what the report counts. See {@link AltTextHealthBaseFilter}.\n *\n * This is not access control: it scopes the aggregate, it does not decide who\n * may see it. Use `access` for that, and note the report is always filtered to\n * the collections the requesting user can read.\n */\n baseFilter?: AltTextHealthBaseFilter\n}\n\n/** Configuration options for the alt text plugin. */\nexport type IncomingAltTextPluginConfig = {\n /**\n * Custom access control for plugin endpoints.\n * Return `true` to allow access, `false` to deny.\n *\n * @default ({ req }) => !!req.user — requires authentication\n */\n access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections to enable the plugin for.\n *\n * Each entry may be a bare collection slug or an object with a `slug` and an\n * optional `mimeTypes` array restricting which MIME types are tracked,\n * validated, and generated. Bare slugs default to `['image/*']`.\n *\n * @example\n * ```typescript\n * collections: [\n * 'images', // shorthand — defaults to ['image/*']\n * { slug: 'media', mimeTypes: ['image/*', 'application/pdf'] },\n * ]\n * ```\n */\n collections: IncomingCollectionsConfig\n\n /** Whether the plugin is enabled. */\n enabled?: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /**\n * Builds the image URL sent to the resolver. See {@link GetImageThumbnail}.\n *\n * @remarks\n * - Prefer a thumbnail/preview size over the original (e.g. from the sizes field)\n * - When the URL transcodes, declare the delivered format via\n * `imageThumbnailMimeType` so source formats the resolver does not accept are\n * not rejected\n */\n getImageThumbnail: GetImageThumbnail\n\n /**\n * Controls the alt text health feature (REST endpoint, cache revalidation hooks, and dashboard widget).\n *\n * - `false` disables the entire feature.\n * - `true` enables it, gated by `access` and covering every document.\n * - An object enables it and configures it. See {@link AltTextHealthCheckConfig}.\n *\n * @default true\n */\n healthCheck?: AltTextHealthCheckConfig | boolean\n\n /**\n * The MIME type `getImageThumbnail` delivers, for every configured collection.\n *\n * Declaring it takes a document's stored `mimeType` out of the decision of\n * whether alt text can be generated. Collections may override it or opt out with\n * `null` — see {@link AltTextCollectionConfig.imageThumbnailMimeType} for the\n * rationale and the `f_auto` caveat.\n *\n * @example 'image/webp'\n */\n imageThumbnailMimeType?: string\n\n /**\n * The locale to generate alt texts in when localization is disabled.\n *\n * Required when localization is disabled, ignored when localization is enabled.\n * @example 'en'\n */\n locale?: string\n\n /**\n * Maximum number of concurrent API requests for bulk generate operations.\n *\n * @default 16\n */\n maxBulkGenerateConcurrency?: number\n\n /**\n * Maximum number of image IDs accepted in a single bulk generate request.\n * Requests exceeding this are rejected with `400`. Duplicate IDs are collapsed\n * before the limit is applied, so each image counts once.\n *\n * Raise it for large libraries that need to process more images per request.\n *\n * @default 100\n */\n maxBulkGenerateIds?: number\n\n /** The resolver to use for generating alt text (e.g., openAIResolver) */\n resolver: AltTextResolver\n}\n\n/** Configuration of the alt text plugin after defaults have been applied. */\nexport type AltTextPluginConfig = {\n /** Access control for plugin endpoints. */\n access: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections with resolved MIME type filters and resolved delivered thumbnail\n * MIME types. The plugin-level `imageThumbnailMimeType` is folded into these\n * entries during normalization, so this is the only place to read it from.\n */\n collections: NormalizedAltTextCollectionConfig[]\n\n /** Whether the plugin is enabled. */\n enabled: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /** Function to get the thumbnail URL of an image document. */\n getImageThumbnail: GetImageThumbnail\n\n /** Whether alt text health tracking is enabled. */\n healthCheck: boolean\n\n /** Access control for the health endpoint. Defaults to `access`. */\n healthCheckAccess: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /** Narrows what the health report counts. See {@link AltTextHealthBaseFilter}. */\n healthCheckBaseFilter?: AltTextHealthBaseFilter\n\n /** The locale to generate alt texts in when localization is disabled. */\n locale?: string\n\n /** The locales to generate alt texts for. */\n locales: string[]\n\n /** Maximum number of concurrent API requests for bulk generate operations. */\n maxBulkGenerateConcurrency: number\n\n /** Maximum number of image IDs accepted per bulk generate request. */\n maxBulkGenerateIds: number\n\n /** The resolver to use for generating alt text */\n resolver: AltTextResolver\n}\n"],"names":[],"mappings":"AAuKA,2EAA2E,GAC3E,WA2CC"}
1
+ {"version":3,"sources":["../../src/types/AltTextPluginConfig.ts"],"sourcesContent":["import type { CollectionSlug, Field, PayloadRequest, Where } from 'payload'\n\nimport type { AltTextResolver } from '../resolvers/types.js'\nimport type {\n AltTextCollectionConfig,\n IncomingCollectionsConfig,\n NormalizedAltTextCollectionConfig,\n} from '../utilities/mimeTypes.js'\n\nexport type { AltTextCollectionConfig, NormalizedAltTextCollectionConfig }\n\n/**\n * Builds the thumbnail URL the resolver fetches. Must be publicly reachable.\n *\n * May be async, so the URL can be signed on demand (S3 presigning, short-lived\n * CDN tokens).\n *\n * @param doc The upload document to build the URL for.\n * @param args.collection The slug of the collection `doc` belongs to — use it to\n * build different URLs per collection (e.g. a Cloudinary transformation for one\n * collection and a plain S3 URL for another).\n * @param args.req The request the generation runs under.\n */\nexport type GetImageThumbnail = (\n doc: Record<string, unknown>,\n args: { collection: CollectionSlug; req: PayloadRequest },\n) => Promise<string> | string\n\n/**\n * Narrows the health scan of one collection to a subset of its documents — in a\n * multi-tenant CMS, to the tenant the request is for. The returned constraint is\n * ANDed onto the scan's MIME type filter.\n *\n * Called once per configured collection, so collections that do not carry the\n * constraining field (a media library shared across tenants, say) can return `{}`\n * instead of being queried for a field they do not have.\n *\n * Return `{}` to scan the collection whole.\n *\n * @param args.collection The slug of the collection being scanned.\n * @param args.req The request the scan runs for.\n */\nexport type AltTextHealthBaseFilter = (args: {\n collection: CollectionSlug\n req: PayloadRequest\n}) => Promise<Where> | Where\n\n/**\n * Narrows the locales a request generates for and is measured against — in a\n * multi-tenant CMS, to the locales of the tenant the request is for.\n *\n * Must return a non-empty subset of `locales`; anything else ends the operation\n * with an error rather than writing into a locale the project does not define.\n *\n * @param args.locales The locales configured on the Payload config.\n * @param args.req The request being served.\n */\nexport type FilterLocales = (args: {\n locales: string[]\n req: PayloadRequest\n}) => Promise<string[]> | string[]\n\n/** Configuration of the alt text health feature. */\nexport type AltTextHealthCheckConfig = {\n /**\n * Access control for the health report — both the REST endpoint and the dashboard\n * widget, which hides itself when this denies.\n *\n * Use it to restrict the collection-wide report more strictly than the\n * per-document generate endpoints (e.g. to admins).\n *\n * @default the plugin's `access`\n */\n access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Narrows what the report counts. See {@link AltTextHealthBaseFilter}.\n *\n * This is not access control: it scopes the aggregate, it does not decide who\n * may see it. Use `access` for that, and note the report is always filtered to\n * the collections the requesting user can read.\n */\n baseFilter?: AltTextHealthBaseFilter\n}\n\n/** Configuration options for the alt text plugin. */\nexport type IncomingAltTextPluginConfig = {\n /**\n * Custom access control for plugin endpoints.\n * Return `true` to allow access, `false` to deny.\n *\n * @default ({ req }) => !!req.user — requires authentication\n */\n access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections to enable the plugin for.\n *\n * Each entry may be a bare collection slug or an object with a `slug` and an\n * optional `mimeTypes` array restricting which MIME types are tracked,\n * validated, and generated. Bare slugs default to `['image/*']`.\n *\n * @example\n * ```typescript\n * collections: [\n * 'images', // shorthand — defaults to ['image/*']\n * { slug: 'media', mimeTypes: ['image/*', 'application/pdf'] },\n * ]\n * ```\n */\n collections: IncomingCollectionsConfig\n\n /** Whether the plugin is enabled. */\n enabled?: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /**\n * Narrows the locales this request targets. See {@link FilterLocales}.\n *\n * Governs bulk generation, the generate endpoint's locale validation, and the\n * health report.\n */\n filterLocales?: FilterLocales\n\n /**\n * Builds the image URL sent to the resolver. See {@link GetImageThumbnail}.\n *\n * @remarks\n * - Prefer a thumbnail/preview size over the original (e.g. from the sizes field)\n * - When the URL transcodes, declare the delivered format via\n * `imageThumbnailMimeType` so source formats the resolver does not accept are\n * not rejected\n */\n getImageThumbnail: GetImageThumbnail\n\n /**\n * Controls the alt text health feature (REST endpoint, cache revalidation hooks, and dashboard widget).\n *\n * - `false` disables the entire feature.\n * - `true` enables it, gated by `access` and covering every document.\n * - An object enables it and configures it. See {@link AltTextHealthCheckConfig}.\n *\n * @default true\n */\n healthCheck?: AltTextHealthCheckConfig | boolean\n\n /**\n * The MIME type `getImageThumbnail` delivers, for every configured collection.\n *\n * Declaring it takes a document's stored `mimeType` out of the decision of\n * whether alt text can be generated. Collections may override it or opt out with\n * `null` — see {@link AltTextCollectionConfig.imageThumbnailMimeType} for the\n * rationale and the `f_auto` caveat.\n *\n * @example 'image/webp'\n */\n imageThumbnailMimeType?: string\n\n /**\n * The locale to generate alt texts in when localization is disabled.\n *\n * Required when localization is disabled, ignored when localization is enabled.\n * @example 'en'\n */\n locale?: string\n\n /**\n * Maximum number of concurrent API requests for bulk generate operations.\n *\n * @default 16\n */\n maxBulkGenerateConcurrency?: number\n\n /**\n * Maximum number of image IDs accepted in a single bulk generate request.\n * Requests exceeding this are rejected with `400`. Duplicate IDs are collapsed\n * before the limit is applied, so each image counts once.\n *\n * Raise it for large libraries that need to process more images per request.\n *\n * @default 100\n */\n maxBulkGenerateIds?: number\n\n /** The resolver to use for generating alt text (e.g., openAIResolver) */\n resolver: AltTextResolver\n}\n\n/** Configuration of the alt text plugin after defaults have been applied. */\nexport type AltTextPluginConfig = {\n /** Access control for plugin endpoints. */\n access: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections with resolved MIME type filters and resolved delivered thumbnail\n * MIME types. The plugin-level `imageThumbnailMimeType` is folded into these\n * entries during normalization, so this is the only place to read it from.\n */\n collections: NormalizedAltTextCollectionConfig[]\n\n /** Whether the plugin is enabled. */\n enabled: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /** Narrows the locales one request targets. See {@link FilterLocales}. */\n filterLocales?: FilterLocales\n\n /** Function to get the thumbnail URL of an image document. */\n getImageThumbnail: GetImageThumbnail\n\n /** Whether alt text health tracking is enabled. */\n healthCheck: boolean\n\n /** Access control for the health endpoint. Defaults to `access`. */\n healthCheckAccess: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /** Narrows what the health report counts. See {@link AltTextHealthBaseFilter}. */\n healthCheckBaseFilter?: AltTextHealthBaseFilter\n\n /** The locale to generate alt texts in when localization is disabled. */\n locale?: string\n\n /** The locales configured on the Payload config. Empty when localization is disabled. */\n locales: string[]\n\n /** Maximum number of concurrent API requests for bulk generate operations. */\n maxBulkGenerateConcurrency: number\n\n /** Maximum number of image IDs accepted per bulk generate request. */\n maxBulkGenerateIds: number\n\n /** The resolver to use for generating alt text */\n resolver: AltTextResolver\n}\n"],"names":[],"mappings":"AA8LA,2EAA2E,GAC3E,WA8CC"}
@@ -3,7 +3,7 @@ import type { AltTextHealthCacheFactory } from './altTextHealthCache.js';
3
3
  export declare const ALT_TEXT_HEALTH_PLUGIN_SLUG = "alt-text";
4
4
  export declare const ALT_TEXT_HEALTH_CACHE_TTL = 3600;
5
5
  export declare const ALT_TEXT_HEALTH_GLOBAL_TAG = "alt-text-health";
6
- export type AltTextHealthErrorCode = 'ALT_TEXT_BASE_FILTER_FAILED' | 'ALT_TEXT_COLLECTION_READ_FAILED' | 'ALT_TEXT_PLUGIN_CONFIG_MISSING';
6
+ export type AltTextHealthErrorCode = 'ALT_TEXT_BASE_FILTER_FAILED' | 'ALT_TEXT_COLLECTION_READ_FAILED' | 'ALT_TEXT_LOCALES_FILTER_FAILED' | 'ALT_TEXT_PLUGIN_CONFIG_MISSING';
7
7
  export type AltTextHealthError = {
8
8
  code: AltTextHealthErrorCode;
9
9
  collection?: string;
@@ -2,6 +2,7 @@ import { unstable_cache } from 'next/cache.js';
2
2
  import { createCachedAltTextHealthScan } from './altTextHealthCache.js';
3
3
  import { localesFromConfig } from './localesFromConfig.js';
4
4
  import { buildMimeTypeWhere } from './mimeTypes.js';
5
+ import { configuredLocales, resolveLocales } from './resolveLocales.js';
5
6
  import { stableStringify } from './stableStringify.js';
6
7
  import { summarizeCollection } from './summarizeCollection.js';
7
8
  export const ALT_TEXT_HEALTH_PLUGIN_SLUG = 'alt-text';
@@ -150,9 +151,6 @@ export const getAltTextHealthCollectionTag = (collectionSlug)=>`${ALT_TEXT_HEALT
150
151
  export async function getAltTextHealthScan(req, cacheFactory = unstable_cache) {
151
152
  const { payload } = req;
152
153
  const pluginConfig = payload.config.custom?.altTextPluginConfig;
153
- const localeCodes = localesFromConfig(payload.config) ?? (pluginConfig?.locale ? [
154
- pluginConfig.locale
155
- ] : []);
156
154
  const isLocalized = Boolean(payload.config.localization);
157
155
  if (!pluginConfig) {
158
156
  return createUnknownScan({
@@ -161,9 +159,36 @@ export async function getAltTextHealthScan(req, cacheFactory = unstable_cache) {
161
159
  message: 'Alt text plugin config not found'
162
160
  },
163
161
  isLocalized,
164
- localeCodes
162
+ localeCodes: localesFromConfig(payload.config) ?? []
165
163
  });
166
164
  }
165
+ const configured = configuredLocales(pluginConfig);
166
+ // Measuring a tenant against locales it does not serve leaves it permanently
167
+ // partial, in a way no editor of that tenant can fix.
168
+ let localeCodes = configured;
169
+ if (pluginConfig.filterLocales) {
170
+ try {
171
+ localeCodes = await resolveLocales({
172
+ pluginConfig,
173
+ req
174
+ });
175
+ } catch (error) {
176
+ const message = error instanceof Error ? error.message : 'Unknown error';
177
+ payload.logger.error({
178
+ err: error,
179
+ msg: 'Alt text health check failed while resolving the locales to report on.',
180
+ plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG
181
+ });
182
+ return createUnknownScan({
183
+ error: {
184
+ code: 'ALT_TEXT_LOCALES_FILTER_FAILED',
185
+ message: `Failed to resolve the alt text health locales: ${message}`
186
+ },
187
+ isLocalized,
188
+ localeCodes: configured
189
+ });
190
+ }
191
+ }
167
192
  const collections = pluginConfig.collections;
168
193
  const resolved = await resolveBaseFilters(req, collections, pluginConfig.healthCheckBaseFilter);
169
194
  if ('error' in resolved) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/altTextHealth.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport { unstable_cache } from 'next/cache.js'\n\nimport type {\n AltTextPluginConfig,\n NormalizedAltTextCollectionConfig,\n} from '../types/AltTextPluginConfig.js'\nimport type { AltTextHealthCacheFactory } from './altTextHealthCache.js'\n\nimport { createCachedAltTextHealthScan } from './altTextHealthCache.js'\nimport { localesFromConfig } from './localesFromConfig.js'\nimport { buildMimeTypeWhere } from './mimeTypes.js'\nimport { stableStringify } from './stableStringify.js'\nimport { summarizeCollection } from './summarizeCollection.js'\n\nexport const ALT_TEXT_HEALTH_PLUGIN_SLUG = 'alt-text'\nexport const ALT_TEXT_HEALTH_CACHE_TTL = 3600\nexport const ALT_TEXT_HEALTH_GLOBAL_TAG = 'alt-text-health'\n\nexport type AltTextHealthErrorCode =\n | 'ALT_TEXT_BASE_FILTER_FAILED'\n | 'ALT_TEXT_COLLECTION_READ_FAILED'\n | 'ALT_TEXT_PLUGIN_CONFIG_MISSING'\n\nexport type AltTextHealthError = {\n code: AltTextHealthErrorCode\n collection?: string\n message: string\n operation?: 'find'\n}\n\nexport type AltTextHealthScanCollection = {\n collection: string\n completeDocs: number\n error?: AltTextHealthError\n invalidDocIds: (number | string)[] | undefined\n missingDocs: number\n partialDocs: number\n totalDocs: number\n}\n\nexport type AltTextHealthScan = {\n checkedAt: string\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCodes: string[]\n}\n\nexport type AltTextHealthWidgetData = {\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCount: number\n totalDocs: number\n}\n\ntype AltTextHealthComputationArgs = {\n /** The resolved base filter per collection slug, if one is configured. */\n baseFilters: Record<string, undefined | Where>\n collections: NormalizedAltTextCollectionConfig[]\n isLocalized: boolean\n localeCodes: string[]\n payload: Payload\n}\n\nconst createUnknownScan = ({\n error,\n isLocalized,\n localeCodes,\n}: {\n error: AltTextHealthScan['errors'][number]\n isLocalized: boolean\n localeCodes: string[]\n}): AltTextHealthScan => ({\n checkedAt: new Date().toISOString(),\n collections: [],\n errors: [error],\n isLocalized,\n localeCodes,\n})\n\nconst createCollectionReadError = (collection: string, message: string) => ({\n code: 'ALT_TEXT_COLLECTION_READ_FAILED' as const,\n collection,\n message,\n operation: 'find' as const,\n})\n\nconst PAGE_SIZE = 500\n\nconst isEmptyWhere = (where: undefined | Where): boolean =>\n !where || Object.keys(where).length === 0\n\nasync function fetchAllDocs(\n payload: Payload,\n collection: string,\n isLocalized: boolean,\n mimeTypes: readonly string[],\n baseFilter: undefined | Where,\n): Promise<{ alt: unknown; id: number | string }[]> {\n const mimeTypeWhere = buildMimeTypeWhere(mimeTypes)\n if (!mimeTypeWhere) {\n return []\n }\n\n // The scan runs with `overrideAccess: true`, so a narrowing constraint has to be\n // part of the query itself — this is what keeps the aggregate within one tenant.\n const where = isEmptyWhere(baseFilter) ? mimeTypeWhere : { and: [mimeTypeWhere, baseFilter!] }\n\n const docs: { alt: unknown; id: number | string }[] = []\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await payload.find({\n collection,\n depth: 0,\n fallbackLocale: isLocalized ? false : undefined,\n limit: PAGE_SIZE,\n locale: isLocalized ? 'all' : undefined,\n overrideAccess: true,\n page,\n select: {\n alt: true,\n },\n where,\n })\n\n for (const doc of result.docs) {\n docs.push({\n id: doc.id,\n alt: 'alt' in doc ? doc.alt : undefined,\n })\n }\n\n hasMore = result.hasNextPage\n page++\n }\n\n return docs\n}\n\nasync function computeAltTextHealthScan({\n baseFilters,\n collections,\n isLocalized,\n localeCodes,\n payload,\n}: AltTextHealthComputationArgs): Promise<AltTextHealthScan> {\n const collectionSummaries = await Promise.all(\n collections.map(async ({ slug, mimeTypes }): Promise<AltTextHealthScanCollection> => {\n try {\n const docs = await fetchAllDocs(payload, slug, isLocalized, mimeTypes, baseFilters[slug])\n\n return summarizeCollection({\n collection: slug,\n docs,\n isLocalized,\n localeCodes,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n const collectionError = createCollectionReadError(slug, message)\n\n payload.logger.error({\n collection: slug,\n err: error,\n msg: 'Alt text health check failed while reading a collection.',\n operation: 'find',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return {\n collection: slug,\n completeDocs: 0,\n error: collectionError,\n invalidDocIds: undefined,\n missingDocs: 0,\n partialDocs: 0,\n totalDocs: 0,\n }\n }\n }),\n )\n\n const errors = collectionSummaries\n .filter((summary) => summary.error)\n .map((summary) => summary.error!)\n\n return {\n checkedAt: new Date().toISOString(),\n collections: collectionSummaries,\n errors,\n isLocalized,\n localeCodes,\n }\n}\n\nexport const getAltTextHealthCollectionTag = (collectionSlug: string): string =>\n `${ALT_TEXT_HEALTH_GLOBAL_TAG}:${collectionSlug}`\n\n/**\n * Resolves the configured base filter for every scanned collection.\n *\n * A throwing filter (a tenant cookie pointing at a deleted tenant, say) must not\n * take the dashboard down, and must never fall back to an unfiltered scan — so it\n * ends the scan with an error the widget and the endpoint report. The error carries\n * the slug in its message rather than in `collection`, so it survives the read-access\n * filter that drops errors for collections the caller cannot read.\n */\nasync function resolveBaseFilters(\n req: PayloadRequest,\n collections: NormalizedAltTextCollectionConfig[],\n baseFilter: AltTextPluginConfig['healthCheckBaseFilter'],\n): Promise<{ baseFilters: Record<string, undefined | Where> } | { error: AltTextHealthError }> {\n const baseFilters: Record<string, undefined | Where> = {}\n\n if (!baseFilter) {\n return { baseFilters }\n }\n\n for (const { slug } of collections) {\n try {\n baseFilters[slug] = await baseFilter({ collection: slug, req })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n\n req.payload.logger.error({\n collection: slug,\n err: error,\n msg: 'Alt text health check failed while resolving the base filter.',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return {\n error: {\n code: 'ALT_TEXT_BASE_FILTER_FAILED',\n message: `Failed to resolve the alt text health base filter for the \"${slug}\" collection: ${message}`,\n },\n }\n }\n }\n\n return { baseFilters }\n}\n\nexport async function getAltTextHealthScan(\n req: PayloadRequest,\n cacheFactory: AltTextHealthCacheFactory<AltTextHealthScan> = unstable_cache,\n): Promise<AltTextHealthScan> {\n const { payload } = req\n const pluginConfig = payload.config.custom?.altTextPluginConfig as AltTextPluginConfig | undefined\n const localeCodes =\n localesFromConfig(payload.config) ?? (pluginConfig?.locale ? [pluginConfig.locale] : [])\n const isLocalized = Boolean(payload.config.localization)\n\n if (!pluginConfig) {\n return createUnknownScan({\n error: {\n code: 'ALT_TEXT_PLUGIN_CONFIG_MISSING',\n message: 'Alt text plugin config not found',\n },\n isLocalized,\n localeCodes,\n })\n }\n\n const collections = pluginConfig.collections\n\n const resolved = await resolveBaseFilters(req, collections, pluginConfig.healthCheckBaseFilter)\n\n if ('error' in resolved) {\n return createUnknownScan({ error: resolved.error, isLocalized, localeCodes })\n }\n\n const { baseFilters } = resolved\n\n const cacheKeyParts = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n [...collections]\n .map(({ slug, mimeTypes }) => `${slug}:${[...mimeTypes].sort().join('|')}`)\n .sort()\n .join(','),\n localeCodes.join(','),\n // The scan is shared across requests, so a scoped scan needs a scoped cache\n // entry. Deriving the key from the resolved filters — rather than taking one\n // from the caller — makes it impossible to narrow the scan without also\n // narrowing its cache, which would serve one tenant's counts to another.\n `filter:${stableStringify(baseFilters)}`,\n ]\n\n const tags = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n ...new Set(collections.map(({ slug }) => getAltTextHealthCollectionTag(slug))),\n ]\n\n const getCachedHealthScan = createCachedAltTextHealthScan({\n cacheFactory,\n cacheKeyParts,\n compute: async () =>\n computeAltTextHealthScan({\n baseFilters,\n collections,\n isLocalized,\n localeCodes,\n payload,\n }),\n revalidate: ALT_TEXT_HEALTH_CACHE_TTL,\n tags,\n })\n\n return getCachedHealthScan()\n}\n\n/**\n * Whether `req.user` is allowed to read the given collection at the collection\n * level. The collection's `read` access is evaluated with the request; `false`\n * denies, while `true` or a scoped `Where` constraint grants visibility of the\n * collection's health aggregate. A thrown access function (e.g. `Forbidden`)\n * counts as denied so a restricted collection never leaks.\n */\nasync function userCanReadCollection(req: PayloadRequest, slug: string): Promise<boolean> {\n const readAccess = req.payload.collections?.[slug]?.config.access?.read\n\n if (typeof readAccess !== 'function') {\n return true\n }\n\n try {\n return (await readAccess({ req })) !== false\n } catch {\n return false\n }\n}\n\n/**\n * Filters a shared, elevated-access health scan down to the collections the\n * requesting user may read. The scan is computed once with `overrideAccess: true`\n * so it stays complete and cacheable; access is applied per request at\n * collection granularity, matching the aggregate's altitude.\n */\nexport async function filterScanByReadAccess(\n req: PayloadRequest,\n scan: AltTextHealthScan,\n): Promise<AltTextHealthScan> {\n const visibility = await Promise.all(\n scan.collections.map((collection) => userCanReadCollection(req, collection.collection)),\n )\n\n const collections = scan.collections.filter((_, index) => visibility[index])\n const allowedSlugs = new Set(collections.map((collection) => collection.collection))\n const errors = scan.errors.filter(\n (error) => !error.collection || allowedSlugs.has(error.collection),\n )\n\n return { ...scan, collections, errors }\n}\n\n/**\n * Whether the requesting user may view the health report at all, per the\n * configured `healthCheck` access gate. The dashboard widget uses this to hide\n * itself, mirroring the gate enforced on the health endpoint.\n */\nexport async function canViewHealthReport(req: PayloadRequest): Promise<boolean> {\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return false\n }\n\n return pluginConfig.healthCheckAccess({ req })\n}\n\nexport function toWidgetData(scan: AltTextHealthScan): AltTextHealthWidgetData {\n return {\n collections: scan.collections,\n errors: scan.errors,\n isLocalized: scan.isLocalized,\n localeCount: scan.localeCodes.length,\n totalDocs: scan.collections.reduce((total, c) => total + c.totalDocs, 0),\n }\n}\n\nexport async function getAltTextHealth(req: PayloadRequest): Promise<AltTextHealthScan> {\n return filterScanByReadAccess(req, await getAltTextHealthScan(req))\n}\n\nexport async function getAltTextHealthWidgetData(\n req: PayloadRequest,\n): Promise<AltTextHealthWidgetData> {\n return toWidgetData(await filterScanByReadAccess(req, await getAltTextHealthScan(req)))\n}\n"],"names":["unstable_cache","createCachedAltTextHealthScan","localesFromConfig","buildMimeTypeWhere","stableStringify","summarizeCollection","ALT_TEXT_HEALTH_PLUGIN_SLUG","ALT_TEXT_HEALTH_CACHE_TTL","ALT_TEXT_HEALTH_GLOBAL_TAG","createUnknownScan","error","isLocalized","localeCodes","checkedAt","Date","toISOString","collections","errors","createCollectionReadError","collection","message","code","operation","PAGE_SIZE","isEmptyWhere","where","Object","keys","length","fetchAllDocs","payload","mimeTypes","baseFilter","mimeTypeWhere","and","docs","page","hasMore","result","find","depth","fallbackLocale","undefined","limit","locale","overrideAccess","select","alt","doc","push","id","hasNextPage","computeAltTextHealthScan","baseFilters","collectionSummaries","Promise","all","map","slug","Error","collectionError","logger","err","msg","plugin","completeDocs","invalidDocIds","missingDocs","partialDocs","totalDocs","filter","summary","getAltTextHealthCollectionTag","collectionSlug","resolveBaseFilters","req","getAltTextHealthScan","cacheFactory","pluginConfig","config","custom","altTextPluginConfig","Boolean","localization","resolved","healthCheckBaseFilter","cacheKeyParts","sort","join","tags","Set","getCachedHealthScan","compute","revalidate","userCanReadCollection","readAccess","access","read","filterScanByReadAccess","scan","visibility","_","index","allowedSlugs","has","canViewHealthReport","healthCheckAccess","toWidgetData","localeCount","reduce","total","c","getAltTextHealth","getAltTextHealthWidgetData"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/utilities/altTextHealth.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport { unstable_cache } from 'next/cache.js'\n\nimport type {\n AltTextPluginConfig,\n NormalizedAltTextCollectionConfig,\n} from '../types/AltTextPluginConfig.js'\nimport type { AltTextHealthCacheFactory } from './altTextHealthCache.js'\n\nimport { createCachedAltTextHealthScan } from './altTextHealthCache.js'\nimport { localesFromConfig } from './localesFromConfig.js'\nimport { buildMimeTypeWhere } from './mimeTypes.js'\nimport { configuredLocales, resolveLocales } from './resolveLocales.js'\nimport { stableStringify } from './stableStringify.js'\nimport { summarizeCollection } from './summarizeCollection.js'\n\nexport const ALT_TEXT_HEALTH_PLUGIN_SLUG = 'alt-text'\nexport const ALT_TEXT_HEALTH_CACHE_TTL = 3600\nexport const ALT_TEXT_HEALTH_GLOBAL_TAG = 'alt-text-health'\n\nexport type AltTextHealthErrorCode =\n | 'ALT_TEXT_BASE_FILTER_FAILED'\n | 'ALT_TEXT_COLLECTION_READ_FAILED'\n | 'ALT_TEXT_LOCALES_FILTER_FAILED'\n | 'ALT_TEXT_PLUGIN_CONFIG_MISSING'\n\nexport type AltTextHealthError = {\n code: AltTextHealthErrorCode\n collection?: string\n message: string\n operation?: 'find'\n}\n\nexport type AltTextHealthScanCollection = {\n collection: string\n completeDocs: number\n error?: AltTextHealthError\n invalidDocIds: (number | string)[] | undefined\n missingDocs: number\n partialDocs: number\n totalDocs: number\n}\n\nexport type AltTextHealthScan = {\n checkedAt: string\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCodes: string[]\n}\n\nexport type AltTextHealthWidgetData = {\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCount: number\n totalDocs: number\n}\n\ntype AltTextHealthComputationArgs = {\n /** The resolved base filter per collection slug, if one is configured. */\n baseFilters: Record<string, undefined | Where>\n collections: NormalizedAltTextCollectionConfig[]\n isLocalized: boolean\n localeCodes: string[]\n payload: Payload\n}\n\nconst createUnknownScan = ({\n error,\n isLocalized,\n localeCodes,\n}: {\n error: AltTextHealthScan['errors'][number]\n isLocalized: boolean\n localeCodes: string[]\n}): AltTextHealthScan => ({\n checkedAt: new Date().toISOString(),\n collections: [],\n errors: [error],\n isLocalized,\n localeCodes,\n})\n\nconst createCollectionReadError = (collection: string, message: string) => ({\n code: 'ALT_TEXT_COLLECTION_READ_FAILED' as const,\n collection,\n message,\n operation: 'find' as const,\n})\n\nconst PAGE_SIZE = 500\n\nconst isEmptyWhere = (where: undefined | Where): boolean =>\n !where || Object.keys(where).length === 0\n\nasync function fetchAllDocs(\n payload: Payload,\n collection: string,\n isLocalized: boolean,\n mimeTypes: readonly string[],\n baseFilter: undefined | Where,\n): Promise<{ alt: unknown; id: number | string }[]> {\n const mimeTypeWhere = buildMimeTypeWhere(mimeTypes)\n if (!mimeTypeWhere) {\n return []\n }\n\n // The scan runs with `overrideAccess: true`, so a narrowing constraint has to be\n // part of the query itself — this is what keeps the aggregate within one tenant.\n const where = isEmptyWhere(baseFilter) ? mimeTypeWhere : { and: [mimeTypeWhere, baseFilter!] }\n\n const docs: { alt: unknown; id: number | string }[] = []\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await payload.find({\n collection,\n depth: 0,\n fallbackLocale: isLocalized ? false : undefined,\n limit: PAGE_SIZE,\n locale: isLocalized ? 'all' : undefined,\n overrideAccess: true,\n page,\n select: {\n alt: true,\n },\n where,\n })\n\n for (const doc of result.docs) {\n docs.push({\n id: doc.id,\n alt: 'alt' in doc ? doc.alt : undefined,\n })\n }\n\n hasMore = result.hasNextPage\n page++\n }\n\n return docs\n}\n\nasync function computeAltTextHealthScan({\n baseFilters,\n collections,\n isLocalized,\n localeCodes,\n payload,\n}: AltTextHealthComputationArgs): Promise<AltTextHealthScan> {\n const collectionSummaries = await Promise.all(\n collections.map(async ({ slug, mimeTypes }): Promise<AltTextHealthScanCollection> => {\n try {\n const docs = await fetchAllDocs(payload, slug, isLocalized, mimeTypes, baseFilters[slug])\n\n return summarizeCollection({\n collection: slug,\n docs,\n isLocalized,\n localeCodes,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n const collectionError = createCollectionReadError(slug, message)\n\n payload.logger.error({\n collection: slug,\n err: error,\n msg: 'Alt text health check failed while reading a collection.',\n operation: 'find',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return {\n collection: slug,\n completeDocs: 0,\n error: collectionError,\n invalidDocIds: undefined,\n missingDocs: 0,\n partialDocs: 0,\n totalDocs: 0,\n }\n }\n }),\n )\n\n const errors = collectionSummaries\n .filter((summary) => summary.error)\n .map((summary) => summary.error!)\n\n return {\n checkedAt: new Date().toISOString(),\n collections: collectionSummaries,\n errors,\n isLocalized,\n localeCodes,\n }\n}\n\nexport const getAltTextHealthCollectionTag = (collectionSlug: string): string =>\n `${ALT_TEXT_HEALTH_GLOBAL_TAG}:${collectionSlug}`\n\n/**\n * Resolves the configured base filter for every scanned collection.\n *\n * A throwing filter (a tenant cookie pointing at a deleted tenant, say) must not\n * take the dashboard down, and must never fall back to an unfiltered scan — so it\n * ends the scan with an error the widget and the endpoint report. The error carries\n * the slug in its message rather than in `collection`, so it survives the read-access\n * filter that drops errors for collections the caller cannot read.\n */\nasync function resolveBaseFilters(\n req: PayloadRequest,\n collections: NormalizedAltTextCollectionConfig[],\n baseFilter: AltTextPluginConfig['healthCheckBaseFilter'],\n): Promise<{ baseFilters: Record<string, undefined | Where> } | { error: AltTextHealthError }> {\n const baseFilters: Record<string, undefined | Where> = {}\n\n if (!baseFilter) {\n return { baseFilters }\n }\n\n for (const { slug } of collections) {\n try {\n baseFilters[slug] = await baseFilter({ collection: slug, req })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n\n req.payload.logger.error({\n collection: slug,\n err: error,\n msg: 'Alt text health check failed while resolving the base filter.',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return {\n error: {\n code: 'ALT_TEXT_BASE_FILTER_FAILED',\n message: `Failed to resolve the alt text health base filter for the \"${slug}\" collection: ${message}`,\n },\n }\n }\n }\n\n return { baseFilters }\n}\n\nexport async function getAltTextHealthScan(\n req: PayloadRequest,\n cacheFactory: AltTextHealthCacheFactory<AltTextHealthScan> = unstable_cache,\n): Promise<AltTextHealthScan> {\n const { payload } = req\n const pluginConfig = payload.config.custom?.altTextPluginConfig as AltTextPluginConfig | undefined\n const isLocalized = Boolean(payload.config.localization)\n\n if (!pluginConfig) {\n return createUnknownScan({\n error: {\n code: 'ALT_TEXT_PLUGIN_CONFIG_MISSING',\n message: 'Alt text plugin config not found',\n },\n isLocalized,\n localeCodes: localesFromConfig(payload.config) ?? [],\n })\n }\n\n const configured = configuredLocales(pluginConfig)\n\n // Measuring a tenant against locales it does not serve leaves it permanently\n // partial, in a way no editor of that tenant can fix.\n let localeCodes = configured\n\n if (pluginConfig.filterLocales) {\n try {\n localeCodes = await resolveLocales({ pluginConfig, req })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n\n payload.logger.error({\n err: error,\n msg: 'Alt text health check failed while resolving the locales to report on.',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return createUnknownScan({\n error: {\n code: 'ALT_TEXT_LOCALES_FILTER_FAILED',\n message: `Failed to resolve the alt text health locales: ${message}`,\n },\n isLocalized,\n localeCodes: configured,\n })\n }\n }\n\n const collections = pluginConfig.collections\n\n const resolved = await resolveBaseFilters(req, collections, pluginConfig.healthCheckBaseFilter)\n\n if ('error' in resolved) {\n return createUnknownScan({ error: resolved.error, isLocalized, localeCodes })\n }\n\n const { baseFilters } = resolved\n\n const cacheKeyParts = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n [...collections]\n .map(({ slug, mimeTypes }) => `${slug}:${[...mimeTypes].sort().join('|')}`)\n .sort()\n .join(','),\n localeCodes.join(','),\n // The scan is shared across requests, so a scoped scan needs a scoped cache\n // entry. Deriving the key from the resolved filters — rather than taking one\n // from the caller — makes it impossible to narrow the scan without also\n // narrowing its cache, which would serve one tenant's counts to another.\n `filter:${stableStringify(baseFilters)}`,\n ]\n\n const tags = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n ...new Set(collections.map(({ slug }) => getAltTextHealthCollectionTag(slug))),\n ]\n\n const getCachedHealthScan = createCachedAltTextHealthScan({\n cacheFactory,\n cacheKeyParts,\n compute: async () =>\n computeAltTextHealthScan({\n baseFilters,\n collections,\n isLocalized,\n localeCodes,\n payload,\n }),\n revalidate: ALT_TEXT_HEALTH_CACHE_TTL,\n tags,\n })\n\n return getCachedHealthScan()\n}\n\n/**\n * Whether `req.user` is allowed to read the given collection at the collection\n * level. The collection's `read` access is evaluated with the request; `false`\n * denies, while `true` or a scoped `Where` constraint grants visibility of the\n * collection's health aggregate. A thrown access function (e.g. `Forbidden`)\n * counts as denied so a restricted collection never leaks.\n */\nasync function userCanReadCollection(req: PayloadRequest, slug: string): Promise<boolean> {\n const readAccess = req.payload.collections?.[slug]?.config.access?.read\n\n if (typeof readAccess !== 'function') {\n return true\n }\n\n try {\n return (await readAccess({ req })) !== false\n } catch {\n return false\n }\n}\n\n/**\n * Filters a shared, elevated-access health scan down to the collections the\n * requesting user may read. The scan is computed once with `overrideAccess: true`\n * so it stays complete and cacheable; access is applied per request at\n * collection granularity, matching the aggregate's altitude.\n */\nexport async function filterScanByReadAccess(\n req: PayloadRequest,\n scan: AltTextHealthScan,\n): Promise<AltTextHealthScan> {\n const visibility = await Promise.all(\n scan.collections.map((collection) => userCanReadCollection(req, collection.collection)),\n )\n\n const collections = scan.collections.filter((_, index) => visibility[index])\n const allowedSlugs = new Set(collections.map((collection) => collection.collection))\n const errors = scan.errors.filter(\n (error) => !error.collection || allowedSlugs.has(error.collection),\n )\n\n return { ...scan, collections, errors }\n}\n\n/**\n * Whether the requesting user may view the health report at all, per the\n * configured `healthCheck` access gate. The dashboard widget uses this to hide\n * itself, mirroring the gate enforced on the health endpoint.\n */\nexport async function canViewHealthReport(req: PayloadRequest): Promise<boolean> {\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return false\n }\n\n return pluginConfig.healthCheckAccess({ req })\n}\n\nexport function toWidgetData(scan: AltTextHealthScan): AltTextHealthWidgetData {\n return {\n collections: scan.collections,\n errors: scan.errors,\n isLocalized: scan.isLocalized,\n localeCount: scan.localeCodes.length,\n totalDocs: scan.collections.reduce((total, c) => total + c.totalDocs, 0),\n }\n}\n\nexport async function getAltTextHealth(req: PayloadRequest): Promise<AltTextHealthScan> {\n return filterScanByReadAccess(req, await getAltTextHealthScan(req))\n}\n\nexport async function getAltTextHealthWidgetData(\n req: PayloadRequest,\n): Promise<AltTextHealthWidgetData> {\n return toWidgetData(await filterScanByReadAccess(req, await getAltTextHealthScan(req)))\n}\n"],"names":["unstable_cache","createCachedAltTextHealthScan","localesFromConfig","buildMimeTypeWhere","configuredLocales","resolveLocales","stableStringify","summarizeCollection","ALT_TEXT_HEALTH_PLUGIN_SLUG","ALT_TEXT_HEALTH_CACHE_TTL","ALT_TEXT_HEALTH_GLOBAL_TAG","createUnknownScan","error","isLocalized","localeCodes","checkedAt","Date","toISOString","collections","errors","createCollectionReadError","collection","message","code","operation","PAGE_SIZE","isEmptyWhere","where","Object","keys","length","fetchAllDocs","payload","mimeTypes","baseFilter","mimeTypeWhere","and","docs","page","hasMore","result","find","depth","fallbackLocale","undefined","limit","locale","overrideAccess","select","alt","doc","push","id","hasNextPage","computeAltTextHealthScan","baseFilters","collectionSummaries","Promise","all","map","slug","Error","collectionError","logger","err","msg","plugin","completeDocs","invalidDocIds","missingDocs","partialDocs","totalDocs","filter","summary","getAltTextHealthCollectionTag","collectionSlug","resolveBaseFilters","req","getAltTextHealthScan","cacheFactory","pluginConfig","config","custom","altTextPluginConfig","Boolean","localization","configured","filterLocales","resolved","healthCheckBaseFilter","cacheKeyParts","sort","join","tags","Set","getCachedHealthScan","compute","revalidate","userCanReadCollection","readAccess","access","read","filterScanByReadAccess","scan","visibility","_","index","allowedSlugs","has","canViewHealthReport","healthCheckAccess","toWidgetData","localeCount","reduce","total","c","getAltTextHealth","getAltTextHealthWidgetData"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js';
3
+ /**
4
+ * The locales one request generates for and is measured against — the configured
5
+ * list itself when no `filterLocales` is set.
6
+ *
7
+ * Throws on an empty or out-of-range result, which would otherwise write into a
8
+ * locale the project does not define.
9
+ */
10
+ export declare function resolveLocales({ pluginConfig, req, }: {
11
+ pluginConfig: AltTextPluginConfig;
12
+ req: PayloadRequest;
13
+ }): Promise<string[]>;
14
+ /** The configured locales, or the single fallback locale when localization is off. */
15
+ export declare function configuredLocales(pluginConfig: AltTextPluginConfig): string[];
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The locales one request generates for and is measured against — the configured
3
+ * list itself when no `filterLocales` is set.
4
+ *
5
+ * Throws on an empty or out-of-range result, which would otherwise write into a
6
+ * locale the project does not define.
7
+ */ export async function resolveLocales({ pluginConfig, req }) {
8
+ const configured = configuredLocales(pluginConfig);
9
+ if (!pluginConfig.filterLocales) {
10
+ return configured;
11
+ }
12
+ // Deduplicated: a repeated locale would be generated and written twice, and
13
+ // reach the resolver as a duplicate key in the schema it must answer with.
14
+ const filtered = [
15
+ ...new Set(await pluginConfig.filterLocales({
16
+ locales: configured,
17
+ req
18
+ }))
19
+ ];
20
+ const unconfigured = filtered.filter((locale)=>!configured.includes(locale));
21
+ if (unconfigured.length > 0) {
22
+ throw new Error(`filterLocales returned ${unconfigured.join(', ')}, which the Payload config does not define. ` + `Configured locales: ${configured.join(', ')}.`);
23
+ }
24
+ if (filtered.length === 0) {
25
+ throw new Error('filterLocales returned no locales, leaving nothing to generate or report on. ' + 'Return at least one of the configured locales.');
26
+ }
27
+ return filtered;
28
+ }
29
+ /** The configured locales, or the single fallback locale when localization is off. */ export function configuredLocales(pluginConfig) {
30
+ if (pluginConfig.locales.length > 0) {
31
+ return pluginConfig.locales;
32
+ }
33
+ return pluginConfig.locale ? [
34
+ pluginConfig.locale
35
+ ] : [];
36
+ }
37
+
38
+ //# sourceMappingURL=resolveLocales.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/resolveLocales.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\n/**\n * The locales one request generates for and is measured against — the configured\n * list itself when no `filterLocales` is set.\n *\n * Throws on an empty or out-of-range result, which would otherwise write into a\n * locale the project does not define.\n */\nexport async function resolveLocales({\n pluginConfig,\n req,\n}: {\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}): Promise<string[]> {\n const configured = configuredLocales(pluginConfig)\n\n if (!pluginConfig.filterLocales) {\n return configured\n }\n\n // Deduplicated: a repeated locale would be generated and written twice, and\n // reach the resolver as a duplicate key in the schema it must answer with.\n const filtered = [...new Set(await pluginConfig.filterLocales({ locales: configured, req }))]\n\n const unconfigured = filtered.filter((locale) => !configured.includes(locale))\n\n if (unconfigured.length > 0) {\n throw new Error(\n `filterLocales returned ${unconfigured.join(', ')}, which the Payload config does not define. ` +\n `Configured locales: ${configured.join(', ')}.`,\n )\n }\n\n if (filtered.length === 0) {\n throw new Error(\n 'filterLocales returned no locales, leaving nothing to generate or report on. ' +\n 'Return at least one of the configured locales.',\n )\n }\n\n return filtered\n}\n\n/** The configured locales, or the single fallback locale when localization is off. */\nexport function configuredLocales(pluginConfig: AltTextPluginConfig): string[] {\n if (pluginConfig.locales.length > 0) {\n return pluginConfig.locales\n }\n\n return pluginConfig.locale ? [pluginConfig.locale] : []\n}\n"],"names":["resolveLocales","pluginConfig","req","configured","configuredLocales","filterLocales","filtered","Set","locales","unconfigured","filter","locale","includes","length","Error","join"],"mappings":"AAIA;;;;;;CAMC,GACD,OAAO,eAAeA,eAAe,EACnCC,YAAY,EACZC,GAAG,EAIJ;IACC,MAAMC,aAAaC,kBAAkBH;IAErC,IAAI,CAACA,aAAaI,aAAa,EAAE;QAC/B,OAAOF;IACT;IAEA,4EAA4E;IAC5E,2EAA2E;IAC3E,MAAMG,WAAW;WAAI,IAAIC,IAAI,MAAMN,aAAaI,aAAa,CAAC;YAAEG,SAASL;YAAYD;QAAI;KAAI;IAE7F,MAAMO,eAAeH,SAASI,MAAM,CAAC,CAACC,SAAW,CAACR,WAAWS,QAAQ,CAACD;IAEtE,IAAIF,aAAaI,MAAM,GAAG,GAAG;QAC3B,MAAM,IAAIC,MACR,CAAC,uBAAuB,EAAEL,aAAaM,IAAI,CAAC,MAAM,4CAA4C,CAAC,GAC7F,CAAC,oBAAoB,EAAEZ,WAAWY,IAAI,CAAC,MAAM,CAAC,CAAC;IAErD;IAEA,IAAIT,SAASO,MAAM,KAAK,GAAG;QACzB,MAAM,IAAIC,MACR,kFACE;IAEN;IAEA,OAAOR;AACT;AAEA,oFAAoF,GACpF,OAAO,SAASF,kBAAkBH,YAAiC;IACjE,IAAIA,aAAaO,OAAO,CAACK,MAAM,GAAG,GAAG;QACnC,OAAOZ,aAAaO,OAAO;IAC7B;IAEA,OAAOP,aAAaU,MAAM,GAAG;QAACV,aAAaU,MAAM;KAAC,GAAG,EAAE;AACzD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhb.software/payload-alt-text-plugin",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "A Payload CMS plugin that adds AI-powered alt text generation for images.",
5
5
  "bugs": "https://github.com/jhb-software/payload-plugins/issues",
6
6
  "repository": {