├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── dev ├── .env.example ├── .gitignore ├── Dockerfile ├── docker-compose.yml ├── jest.config.js ├── nodemon.json ├── package.json ├── plugin.spec.ts ├── src │ ├── blocks │ │ └── TextBlock │ │ │ └── index.ts │ ├── collections │ │ ├── Examples.ts │ │ ├── ExamplesWithVersions.ts │ │ ├── Media.ts │ │ └── Users.ts │ ├── media │ │ ├── IMG_3624-1024x768.jpg │ │ └── IMG_3624.jpeg │ ├── mocks │ │ └── fileStub.js │ ├── payload.config.ts │ └── server.ts └── tsconfig.json ├── eslint-config ├── index.js └── rules │ ├── import.js │ ├── prettier.js │ ├── style.js │ └── typescript.js ├── package.json ├── src ├── access │ ├── admins.ts │ ├── adminsOrPublished.ts │ ├── anyone.ts │ ├── checkRole.ts │ └── validateAccess.ts ├── aiCaption.ts ├── aiTranslate.ts ├── components │ ├── AfterDashboard │ │ ├── index.scss │ │ └── index.tsx │ ├── Metadata │ │ ├── index.scss │ │ └── index.tsx │ └── Translator │ │ ├── Translator.scss │ │ └── index.tsx ├── countries.json ├── deepCompareAndMerge.ts ├── fsMock.js ├── generateImage.ts ├── generateText.ts ├── handleMissingTranslate.ts ├── handleTranslate.ts ├── index.ts ├── mocks │ └── mockFile.js ├── onInitExtension.ts ├── plugin.ts ├── seoTools.ts ├── stringTranslations.ts ├── translateTextAndObjects.ts ├── types.ts └── webpack.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = null 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['./eslint-config'], 4 | overrides: [ 5 | // Temporary overrides 6 | { 7 | files: ['dev/**/*.ts'], 8 | rules: { 9 | 'import/no-relative-packages': 'off', 10 | 'no-process-env': 'off', 11 | }, 12 | }, 13 | ], 14 | excludes: [ 15 | 'dev/plugin.spec.ts', 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dev/tmp 2 | dev/yarn.lock 3 | 4 | # Created by https://www.gitignore.io/api/node,macos,windows,webstorm,sublimetext,visualstudiocode 5 | 6 | ### macOS ### 7 | *.DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | ### Node ### 31 | # Logs 32 | logs 33 | *.log 34 | npm-debug.log* 35 | yarn-debug.log* 36 | yarn-error.log* 37 | 38 | # Runtime data 39 | pids 40 | *.pid 41 | *.seed 42 | *.pid.lock 43 | 44 | # Directory for instrumented libs generated by jscoverage/JSCover 45 | lib-cov 46 | 47 | # Coverage directory used by tools like istanbul 48 | coverage 49 | 50 | # nyc test coverage 51 | .nyc_output 52 | 53 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 54 | .grunt 55 | 56 | # Bower dependency directory (https://bower.io/) 57 | bower_components 58 | 59 | # node-waf configuration 60 | .lock-wscript 61 | 62 | # Compiled binary addons (http://nodejs.org/api/addons.html) 63 | build/Release 64 | 65 | # Dependency directories 66 | node_modules/ 67 | jspm_packages/ 68 | 69 | # Typescript v1 declaration files 70 | typings/ 71 | 72 | # Optional npm cache directory 73 | .npm 74 | 75 | # Optional eslint cache 76 | .eslintcache 77 | 78 | # Optional REPL history 79 | .node_repl_history 80 | 81 | # Output of 'npm pack' 82 | *.tgz 83 | 84 | # Yarn Integrity file 85 | .yarn-integrity 86 | 87 | # Yarn Berry 88 | .yarn/* 89 | !.yarn/patches 90 | !.yarn/plugins 91 | !.yarn/releases 92 | !.yarn/sdks 93 | !.yarn/versions 94 | .pnp.* 95 | 96 | # dotenv environment variables file 97 | .env 98 | 99 | 100 | ### SublimeText ### 101 | # cache files for sublime text 102 | *.tmlanguage.cache 103 | *.tmPreferences.cache 104 | *.stTheme.cache 105 | 106 | # workspace files are user-specific 107 | *.sublime-workspace 108 | 109 | # project files should be checked into the repository, unless a significant 110 | # proportion of contributors will probably not be using SublimeText 111 | # *.sublime-project 112 | 113 | # sftp configuration file 114 | sftp-config.json 115 | 116 | # Package control specific files 117 | Package Control.last-run 118 | Package Control.ca-list 119 | Package Control.ca-bundle 120 | Package Control.system-ca-bundle 121 | Package Control.cache/ 122 | Package Control.ca-certs/ 123 | Package Control.merged-ca-bundle 124 | Package Control.user-ca-bundle 125 | oscrypto-ca-bundle.crt 126 | bh_unicode_properties.cache 127 | 128 | # Sublime-github package stores a github token in this file 129 | # https://packagecontrol.io/packages/sublime-github 130 | GitHub.sublime-settings 131 | 132 | ### VisualStudioCode ### 133 | .vscode/* 134 | !.vscode/tasks.json 135 | !.vscode/launch.json 136 | !.vscode/extensions.json 137 | .history 138 | 139 | ### WebStorm ### 140 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 141 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 142 | 143 | .idea/* 144 | # User-specific stuff: 145 | .idea/**/workspace.xml 146 | .idea/**/tasks.xml 147 | .idea/dictionaries 148 | 149 | # Sensitive or high-churn files: 150 | .idea/**/dataSources/ 151 | .idea/**/dataSources.ids 152 | .idea/**/dataSources.xml 153 | .idea/**/dataSources.local.xml 154 | .idea/**/sqlDataSources.xml 155 | .idea/**/dynamic.xml 156 | .idea/**/uiDesigner.xml 157 | 158 | # Gradle: 159 | .idea/**/gradle.xml 160 | .idea/**/libraries 161 | 162 | # CMake 163 | cmake-build-debug/ 164 | 165 | # Mongo Explorer plugin: 166 | .idea/**/mongoSettings.xml 167 | 168 | ## File-based project format: 169 | *.iws 170 | 171 | ## Plugin-specific files: 172 | 173 | # IntelliJ 174 | /out/ 175 | 176 | # mpeltonen/sbt-idea plugin 177 | .idea_modules/ 178 | 179 | # JIRA plugin 180 | atlassian-ide-plugin.xml 181 | 182 | # Cursive Clojure plugin 183 | .idea/replstate.xml 184 | 185 | # Ruby plugin and RubyMine 186 | /.rakeTasks 187 | 188 | # Crashlytics plugin (for Android Studio and IntelliJ) 189 | com_crashlytics_export_strings.xml 190 | crashlytics.properties 191 | crashlytics-build.properties 192 | fabric.properties 193 | 194 | ### WebStorm Patch ### 195 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 196 | 197 | # *.iml 198 | # modules.xml 199 | # .idea/misc.xml 200 | # *.ipr 201 | 202 | # Sonarlint plugin 203 | .idea/sonarlint 204 | 205 | ### Windows ### 206 | # Windows thumbnail cache files 207 | Thumbs.db 208 | ehthumbs.db 209 | ehthumbs_vista.db 210 | 211 | # Folder config file 212 | Desktop.ini 213 | 214 | # Recycle Bin used on file shares 215 | $RECYCLE.BIN/ 216 | 217 | # Windows Installer files 218 | *.cab 219 | *.msi 220 | *.msm 221 | *.msp 222 | 223 | # Windows shortcuts 224 | *.lnk 225 | 226 | # End of https://www.gitignore.io/api/node,macos,windows,webstorm,sublimetext,visualstudiocode 227 | 228 | # Ignore all uploads 229 | demo/upload 230 | demo/media 231 | demo/files 232 | 233 | # Ignore build folder 234 | build 235 | 236 | # Ignore built components 237 | components/index.js 238 | components/styles.css 239 | 240 | # Ignore generated 241 | dev/generated-types.ts 242 | dev/generated-schema.graphql 243 | 244 | # Ignore dist, no need for git 245 | dist 246 | 247 | dev/src/uploads 248 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | parser: "typescript", 4 | semi: false, 5 | singleQuote: true, 6 | trailingComma: "all", 7 | arrowParens: "avoid", 8 | }; 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 wirewire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Payload AI 2 | 3 | Translate content to different languages using [OpenAI's GPT](https://openai.com/). 4 | 5 | 6 | ### How to install the plugin 7 | 8 | Install via npm: 9 | 10 | 11 | ``` 12 | npm install payload-ai 13 | ``` 14 | 15 | Or yarn: 16 | ``` 17 | yarn add payload-ai 18 | ``` 19 | 20 | To install the plugin, simply add it to your payload.config() in the Plugin array. 21 | 22 | ```ts 23 | import payloadAi from 'payload-ai'; 24 | 25 | export const config = buildConfig({ 26 | plugins: [ 27 | // You can pass options to the plugin 28 | payloadAi({ 29 | enabled: true, 30 | }), 31 | ] 32 | }); 33 | ``` 34 | 35 | ### Collection translation 📦 36 | 37 | Add the `collections` where you want to enable the translation and the `fields`. It will translate each field (also nested fields) on every update of the default language. 38 | 39 | ```ts 40 | plugins: [ 41 | aiTranslatorPlugin({ 42 | enabled: true, 43 | collections: { 44 | examples: { // Name of the collection you want to add translations 45 | fields: [ 46 | 'stringText', // Keys of fields you want to translate (wil also translate nested fields) 47 | 'richText', 48 | ], 49 | }, 50 | }, 51 | }), 52 | ], 53 | ``` 54 | 55 | 56 | #### Custom prompts by Field 57 | 58 | Use `promptFunc` for each field to customize the prompt. 59 | 60 | ```jsx 61 | plugins: [ 62 | aiTranslatorPlugin({ 63 | enabled: true, 64 | collections: { 65 | examples: { 66 | settings: { 67 | model: 'gpt-4', 68 | promptFunc: ({ messages, namespace }) => { 69 | return [ 70 | { 71 | role: 'system', 72 | content: 73 | 'Important: Add a smily face at the end of the message to make the AI more friendly. 😊', 74 | }, 75 | ...messages, 76 | ] 77 | }, 78 | }, 79 | }, 80 | } 81 | } 82 | ] 83 | ``` 84 | 85 | 86 | The function will allow you to use the following 87 | 88 | - `req`: Request 89 | - `doc` Document in languages 90 | - `previousDoc` Old document (only available on Update) 91 | - `targetDoc` The old target document 92 | - `collectionOptions` 93 | - `language` 94 | - translatorConfig 95 | language: string, 96 | sourceLanguage?: string, 97 | 98 | - targetField 99 | - sourceField 100 | 101 | 102 | 103 | ### Use with [payload-seo](https://payloadcms.com/docs/plugins/seo) 104 | 105 | ```jsx 106 | 107 | 108 | import {generateTitle, generateDescription } from "payload-ai"; 109 | 110 | seo({ 111 | collections: ['examples'], 112 | // uploadsCollection: 'media', 113 | generateTitle: generateTitle, 114 | generateDescription: ({ doc }) => generateDescription, 115 | }); 116 | ``` 117 | 118 | ### String translation 119 | 120 | Use this to provide a [backend](https://github.com/i18next/i18next-http-backend) for [i18next](https://www.i18next.com) string translations. 121 | 122 | 123 | ```jsx 124 | plugins: [ 125 | aiTranslatorPlugin({ 126 | enabled: true, 127 | stringTranslation: { 128 | enabled: true 129 | } 130 | }), 131 | ], 132 | ``` 133 | #### Change model for string translation 134 | 135 | To update the model you can change the collection settings in the same way as with other collections. 136 | 137 | ```jsx 138 | plugins: [ 139 | aiTranslatorPlugin({ 140 | enabled: true, 141 | stringTranslation: { 142 | enabled: true 143 | } 144 | collections: { 145 | translations: { 146 | settings: { 147 | model: 'gpt-4', 148 | }, 149 | } 150 | } 151 | }), 152 | ], 153 | ``` 154 | 155 | ### Access control 156 | 157 | By default the plugin will use the [update](https://payloadcms.com/docs/access-control/collections#update) access control of the collection. 158 | 159 | To overwrite that behaviour you can add `access` to the collections configuration. 160 | 161 | 162 | ```jsx 163 | plugins: [ 164 | aiTranslatorPlugin({ 165 | enabled: true, 166 | stringTranslation: { 167 | enabled: true 168 | } 169 | collections: { 170 | examples: { 171 | access: () => true, 172 | } 173 | } 174 | }), 175 | ], 176 | ``` 177 | 178 | 179 | ### Planned features 🧭 180 | 181 | - generate image alt text from GPT 182 | - generate SEO Text 183 | - generate structured content 184 | - custom access control 185 | - custom overrides for translation 186 | - generate images based on input 187 | - generate Open Graph based on content 188 | 189 | #### Use in hooks 190 | 191 | TODO: add documentation 192 | 193 | myCollectionPrompt = ({source}) => { 194 | 195 | source() 196 | 197 | return 198 | } 199 | -------------------------------------------------------------------------------- /dev/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URI=mongodb://127.0.0.1/plugin-development 2 | PAYLOAD_SECRET=hellohereisasecretforyou 3 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /dev/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | /media 4 | node_modules 5 | .DS_Store 6 | .env 7 | -------------------------------------------------------------------------------- /dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.8-alpine as base 2 | 3 | FROM base as builder 4 | 5 | WORKDIR /home/node/app 6 | COPY package*.json ./ 7 | 8 | COPY . . 9 | RUN yarn install 10 | RUN yarn build 11 | 12 | FROM base as runtime 13 | 14 | ENV NODE_ENV=production 15 | ENV PAYLOAD_CONFIG_PATH=dist/payload.config.js 16 | 17 | WORKDIR /home/node/app 18 | COPY package*.json ./ 19 | COPY yarn.lock ./ 20 | 21 | RUN yarn install --production 22 | COPY --from=builder /home/node/app/dist ./dist 23 | COPY --from=builder /home/node/app/build ./build 24 | 25 | EXPOSE 3000 26 | 27 | CMD ["node", "dist/server.js"] 28 | -------------------------------------------------------------------------------- /dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | payload: 5 | image: node:18-alpine 6 | ports: 7 | - '3000:3000' 8 | volumes: 9 | - .:/home/node/app 10 | - node_modules:/home/node/app/node_modules 11 | working_dir: /home/node/app/ 12 | command: sh -c "yarn install && yarn dev" 13 | depends_on: 14 | - mongo 15 | env_file: 16 | - .env 17 | 18 | mongo: 19 | image: mongo:latest 20 | ports: 21 | - '27017:27017' 22 | command: 23 | - --storageEngine=wiredTiger 24 | volumes: 25 | - data:/data/db 26 | logging: 27 | driver: none 28 | 29 | volumes: 30 | data: 31 | node_modules: 32 | -------------------------------------------------------------------------------- /dev/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | testEnvironment: 'node', 4 | transform: { 5 | '^.+\\.(t|j)sx?$': '/node_modules/@swc/jest', 6 | }, 7 | moduleNameMapper: { 8 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 9 | '/src/mocks/fileStub.js', 10 | '\\.(css|scss)$': '/src/mocks/fileStub.js', 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /dev/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "ext": "ts", 4 | "exec": "ts-node src/server.ts -- -I", 5 | "stdin": false 6 | } -------------------------------------------------------------------------------- /dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Payload2Blank", 3 | "description": "A blank template to get started with Payload", 4 | "version": "1.0.0", 5 | "main": "dist/server.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon", 9 | "build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build", 10 | "build:server": "tsc", 11 | "build": "yarn copyfiles && yarn build:payload && yarn build:server", 12 | "serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js", 13 | "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", 14 | "generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types", 15 | "generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema", 16 | "payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload" 17 | }, 18 | "dependencies": { 19 | "@payloadcms/bundler-webpack": "^1.0.0", 20 | "@payloadcms/db-mongodb": "^1.2.0", 21 | "@payloadcms/plugin-cloud": "^2.0.0", 22 | "@payloadcms/plugin-seo": "^1.0.15", 23 | "@payloadcms/richtext-lexical": "^0.5.2", 24 | "@payloadcms/richtext-slate": "^1.3.1", 25 | "cross-env": "^7.0.3", 26 | "dotenv": "^8.2.0", 27 | "express": "^4.17.1", 28 | "payload": "^2.8.2" 29 | }, 30 | "devDependencies": { 31 | "@types/express": "^4.17.9", 32 | "copyfiles": "^2.4.1", 33 | "nodemon": "^2.0.6", 34 | "ts-node": "^9.1.1", 35 | "typescript": "^4.8.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /dev/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from 'http' 2 | import mongoose from 'mongoose' 3 | import payload from 'payload' 4 | import { start } from './src/server' 5 | 6 | describe('Plugin tests', () => { 7 | let server: Server 8 | 9 | beforeAll(async () => { 10 | await start({ local: true }) 11 | }) 12 | 13 | afterAll(async () => { 14 | await mongoose.connection.dropDatabase() 15 | await mongoose.connection.close() 16 | server.close() 17 | }) 18 | 19 | // Add tests to ensure that the plugin works as expected 20 | 21 | // Example test to check for seeded data 22 | it('seeds data accordingly', async () => { 23 | const newCollectionQuery = await payload.find({ 24 | collection: 'new-collection', 25 | sort: 'createdAt', 26 | }) 27 | 28 | expect(newCollectionQuery.totalDocs).toEqual(1) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /dev/src/blocks/TextBlock/index.ts: -------------------------------------------------------------------------------- 1 | import type { Block } from 'payload/types' 2 | 3 | export const TextBlock: Block = { 4 | slug: 'textblock', 5 | fields: [ 6 | { 7 | name: 'name', 8 | type: 'text', 9 | }, 10 | { 11 | name: 'content', 12 | type: 'richText', 13 | }, 14 | { 15 | name: 'kind', 16 | type: 'select', 17 | options: [ 18 | { 19 | label: 'Info', 20 | value: 'info', 21 | }, 22 | { 23 | label: 'Warning', 24 | value: 'warning', 25 | }, 26 | { 27 | label: 'Error', 28 | value: 'error', 29 | }, 30 | ], 31 | }, 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /dev/src/collections/Examples.ts: -------------------------------------------------------------------------------- 1 | import { lexicalEditor } from '@payloadcms/richtext-lexical' 2 | import { CollectionConfig } from 'payload/types' 3 | 4 | // Example Collection - For reference only, this must be added to payload.config.ts to be used. 5 | const Examples: CollectionConfig = { 6 | slug: 'examples', 7 | admin: { 8 | useAsTitle: 'title', 9 | }, 10 | access: { 11 | update: () => false, 12 | }, 13 | fields: [ 14 | { 15 | name: 'title', 16 | type: 'text', 17 | localized: true, 18 | }, 19 | { 20 | name: 'contentRichText', 21 | type: 'richText', 22 | //editor: lexicalEditor({}), 23 | localized: true, 24 | }, 25 | { 26 | name: 'longText', 27 | type: 'code', 28 | localized: true, 29 | }, 30 | { 31 | name: 'doNotAutoTranslate', 32 | type: 'code', 33 | localized: true, 34 | }, 35 | ], 36 | } 37 | 38 | export default Examples 39 | -------------------------------------------------------------------------------- /dev/src/collections/ExamplesWithVersions.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types' 2 | 3 | // Example Collection - For reference only, this must be added to payload.config.ts to be used. 4 | const ExamplesWithVersions: CollectionConfig = { 5 | slug: 'examples-with-versions', 6 | admin: { 7 | useAsTitle: 'title', 8 | }, 9 | versions: { 10 | drafts: true, 11 | }, 12 | fields: [ 13 | { 14 | name: 'title', 15 | type: 'text', 16 | localized: true, 17 | }, 18 | { 19 | name: 'longText', 20 | type: 'code', 21 | localized: true, 22 | }, 23 | { 24 | name: 'doNotAutoTranslate', 25 | type: 'code', 26 | localized: true, 27 | }, 28 | ], 29 | } 30 | 31 | export default ExamplesWithVersions 32 | -------------------------------------------------------------------------------- /dev/src/collections/Media.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types' 2 | 3 | export const Media: CollectionConfig = { 4 | slug: 'media', 5 | access: { 6 | read: () => true, 7 | }, 8 | upload: { 9 | staticURL: '/media', 10 | staticDir: 'media', 11 | imageSizes: [ 12 | { 13 | name: 'thumbnail', 14 | width: 400, 15 | height: 300, 16 | position: 'centre', 17 | }, 18 | { 19 | name: 'card', 20 | width: 768, 21 | height: 1024, 22 | position: 'centre', 23 | }, 24 | { 25 | name: 'tablet', 26 | width: 1024, 27 | // By specifying `undefined` or leaving a height undefined, 28 | // the image will be sized to a certain width, 29 | // but it will retain its original aspect ratio 30 | // and calculate a height automatically. 31 | height: undefined, 32 | position: 'centre', 33 | }, 34 | ], 35 | adminThumbnail: 'thumbnail', 36 | mimeTypes: ['image/*'], 37 | }, 38 | fields: [ 39 | { 40 | name: 'alt', 41 | type: 'text', 42 | }, 43 | ], 44 | } 45 | -------------------------------------------------------------------------------- /dev/src/collections/Users.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | 3 | const Users: CollectionConfig = { 4 | slug: 'users', 5 | auth: true, 6 | admin: { 7 | useAsTitle: 'email', 8 | }, 9 | fields: [ 10 | // Email added by default 11 | // Add more fields as needed 12 | ], 13 | }; 14 | 15 | export default Users; -------------------------------------------------------------------------------- /dev/src/media/IMG_3624-1024x768.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wirewirewirewire/payload-ai/6a5eb67bc64ace532408da585af7563072b56ef3/dev/src/media/IMG_3624-1024x768.jpg -------------------------------------------------------------------------------- /dev/src/media/IMG_3624.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wirewirewirewire/payload-ai/6a5eb67bc64ace532408da585af7563072b56ef3/dev/src/media/IMG_3624.jpeg -------------------------------------------------------------------------------- /dev/src/mocks/fileStub.js: -------------------------------------------------------------------------------- 1 | export default 'file-stub' 2 | -------------------------------------------------------------------------------- /dev/src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { buildConfig } from 'payload/config' 2 | import path from 'path' 3 | import Users from './collections/Users' 4 | import Examples from './collections/Examples' 5 | import { Media } from './collections/Media' 6 | import { mongooseAdapter } from '@payloadcms/db-mongodb' 7 | import { webpackBundler } from '@payloadcms/bundler-webpack' 8 | //import { slateEditor } from '@payloadcms/richtext-slate' 9 | 10 | import { 11 | BlocksFeature, 12 | BoldTextFeature, 13 | LinkFeature, 14 | lexicalEditor, 15 | } from '@payloadcms/richtext-lexical' 16 | import { aiTranslatorPlugin, generateDescription, generateTitle } from '../../src/index' 17 | import ExamplesWithVersions from './collections/ExamplesWithVersions' 18 | import seo from '@payloadcms/plugin-seo' 19 | import { TextBlock } from './blocks/TextBlock' 20 | 21 | export default buildConfig({ 22 | admin: { 23 | user: Users.slug, 24 | bundler: webpackBundler(), 25 | webpack: config => { 26 | const newConfig = { 27 | ...config, 28 | resolve: { 29 | ...config.resolve, 30 | alias: { 31 | ...(config?.resolve?.alias || {}), 32 | react: path.join(__dirname, '../node_modules/react'), 33 | 'react-dom': path.join(__dirname, '../node_modules/react-dom'), 34 | payload: path.join(__dirname, '../node_modules/payload'), 35 | // payload: path.join(__dirname, '../node_modules/payload'), 36 | // '@faceless-ui/modal': path.join(__dirname, '../node_modules/@faceless-ui/modal'), 37 | }, 38 | }, 39 | } 40 | return newConfig 41 | }, 42 | }, 43 | collections: [Examples, ExamplesWithVersions, Users, Media], 44 | //editor: slateEditor({}), 45 | 46 | editor: lexicalEditor({ 47 | features: ({ defaultFeatures }) => [ 48 | ...defaultFeatures, 49 | 50 | LinkFeature({ 51 | // Example showing how to customize the built-in fields 52 | // of the Link feature 53 | fields: [ 54 | { 55 | name: 'rel', 56 | label: 'Rel Attribute', 57 | type: 'select', 58 | hasMany: true, 59 | options: ['noopener', 'noreferrer', 'nofollow'], 60 | admin: { 61 | description: 62 | 'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.', 63 | }, 64 | }, 65 | ], 66 | }), 67 | BlocksFeature({ 68 | blocks: [TextBlock], 69 | }), 70 | ], 71 | }), 72 | localization: { 73 | locales: ['en', 'de' /*'ja' 'es','fr', 'it', 'ja' */], 74 | defaultLocale: 'en', 75 | fallback: true, 76 | }, 77 | typescript: { 78 | outputFile: path.resolve(__dirname, 'payload-types.ts'), 79 | }, 80 | graphQL: { 81 | schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'), 82 | }, 83 | plugins: [ 84 | aiTranslatorPlugin({ 85 | enabled: true, 86 | collections: { 87 | examples: { 88 | fields: ['title', 'longText', 'jsonContent', 'contentRichText'], 89 | }, 90 | 'examples-with-versions': { 91 | fields: ['title', 'longText', 'jsonContent'], 92 | }, 93 | media: { 94 | fields: ['alt'], 95 | }, 96 | translations: { 97 | access: () => true, 98 | settings: { 99 | model: 'gpt-4', 100 | promptFunc: ({ messages, namespace }) => { 101 | console.log('promptFunc', messages, namespace) 102 | 103 | return [ 104 | { 105 | role: 'system', 106 | content: 107 | 'Important: Add a smily face at the end of the message to make the AI more friendly. 😊', 108 | }, 109 | ...messages, 110 | ] 111 | }, 112 | }, 113 | }, 114 | }, 115 | }), 116 | seo({ 117 | collections: ['examples'], 118 | // uploadsCollection: 'media', 119 | generateTitle: generateTitle, 120 | generateDescription: generateDescription, 121 | }), 122 | ], 123 | db: mongooseAdapter({ 124 | url: process.env.DATABASE_URI, 125 | }), 126 | }) 127 | -------------------------------------------------------------------------------- /dev/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import payload from 'payload' 3 | import { InitOptions } from 'payload/config' 4 | 5 | require('dotenv').config() 6 | const app = express() 7 | 8 | // Redirect root to Admin panel 9 | app.get('/', (_, res) => { 10 | res.redirect('/admin') 11 | }) 12 | 13 | export const start = async (args?: Partial) => { 14 | // Initialize Payload 15 | await payload.init({ 16 | secret: process.env.PAYLOAD_SECRET, 17 | express: app, 18 | onInit: async () => { 19 | payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`) 20 | }, 21 | ...(args || {}), 22 | }) 23 | 24 | app.listen(3000) 25 | } 26 | 27 | start() 28 | -------------------------------------------------------------------------------- /dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "strict": false, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "outDir": "./dist", 14 | "rootDir": "./src", 15 | "jsx": "react", 16 | "paths": { 17 | "payload/generated-types": [ 18 | "./src/payload-types.ts" 19 | ] 20 | } 21 | }, 22 | "include": [ 23 | "src" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "dist", 28 | "build" 29 | ], 30 | "ts-node": { 31 | "transpileOnly": true, 32 | "swc": true 33 | } 34 | } -------------------------------------------------------------------------------- /eslint-config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'airbnb-base', 5 | require.resolve('./rules/style.js'), 6 | require.resolve('./rules/import.js'), 7 | require.resolve('./rules/typescript.js'), 8 | require.resolve('./rules/prettier.js'), 9 | ], 10 | env: { 11 | es6: true, 12 | browser: true, 13 | node: true, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /eslint-config/rules/import.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | }, 5 | extends: ['plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript'], 6 | plugins: ['import'], 7 | settings: { 8 | 'import/parsers': { 9 | '@typescript-eslint/parser': ['.ts'], 10 | }, 11 | }, 12 | rules: { 13 | /** 14 | * https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-unresolved.md 15 | */ 16 | 'import/no-unresolved': ['error', { commonjs: true, caseSensitive: true }], 17 | 'import/no-default-export': 'off', 18 | 'import/prefer-default-export': 'off', 19 | 'import/extensions': [ 20 | 'error', 21 | 'ignorePackages', 22 | { 23 | ts: 'never', 24 | tsx: 'never', 25 | js: 'never', 26 | jsx: 'never', 27 | }, 28 | ], 29 | 'import/no-extraneous-dependencies': 'off', 30 | /** 31 | * https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/named.md#when-not-to-use-it 32 | */ 33 | 'import/named': 'error', 34 | 'import/no-relative-packages': 'warn', 35 | 'import/no-import-module-exports': 'warn', 36 | 'import/no-cycle': 'warn', 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /eslint-config/rules/prettier.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier'], 3 | extends: ['plugin:prettier/recommended'], 4 | rules: { 5 | 'prettier/prettier': 'error', 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /eslint-config/rules/style.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'prefer-named-exports': 'off', 4 | 5 | 'prefer-destructuring': 'off', 6 | // 'prefer-destructuring': ['warn', { object: true, array: true }], 7 | // ensure all object/arrays end with a comma 8 | 'comma-dangle': ['error', 'always-multiline'], 9 | 'class-methods-use-this': 'off', 10 | // consistent new lines 11 | 'function-paren-newline': ['error', 'consistent'], 12 | 'eol-last': ['error', 'always'], 13 | // allow restricted syntax like for...of loops 14 | 'no-restricted-syntax': 'off', 15 | 'no-await-in-loop': 'off', 16 | 'no-console': 'error', 17 | // 'no-floating-promises': true, 18 | // do not allow process.env access in files 19 | 'no-process-env': 'warn', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /eslint-config/rules/typescript.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['@typescript-eslint'], 3 | overrides: [ 4 | { 5 | files: ['**/**.ts', '**/**.d.ts'], 6 | rules: { 7 | 'no-undef': 'off', 8 | camelcase: 'off', 9 | '@typescript-eslint/adjacent-overload-signatures': 'error', 10 | /** 11 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/array-type.md 12 | */ 13 | '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], 14 | /** 15 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/await-thenable.md 16 | */ 17 | '@typescript-eslint/await-thenable': 'off', 18 | /** 19 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-assertions.md 20 | */ 21 | '@typescript-eslint/consistent-type-assertions': [ 22 | 'error', 23 | { assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' }, 24 | ], 25 | /** 26 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-definitions.md 27 | */ 28 | '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], 29 | /** 30 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-imports.md 31 | */ 32 | '@typescript-eslint/consistent-type-imports': 'warn', 33 | /** 34 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-function-return-type.md 35 | */ 36 | '@typescript-eslint/explicit-function-return-type': [ 37 | 'error', 38 | { 39 | // TODO: come back and check if we need those 40 | allowExpressions: true, 41 | allowTypedFunctionExpressions: true, 42 | allowHigherOrderFunctions: true, 43 | allowConciseArrowFunctionExpressionsStartingWithVoid: false, 44 | }, 45 | ], 46 | /** 47 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md 48 | */ 49 | '@typescript-eslint/explicit-member-accessibility': [ 50 | 'error', 51 | { accessibility: 'no-public' }, 52 | ], 53 | /** 54 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md 55 | */ 56 | '@typescript-eslint/member-delimiter-style': [ 57 | 'error', 58 | { 59 | multiline: { 60 | delimiter: 'none', 61 | requireLast: true, 62 | }, 63 | singleline: { 64 | delimiter: 'semi', 65 | requireLast: false, 66 | }, 67 | }, 68 | ], 69 | /** 70 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/method-signature-style.md 71 | */ 72 | '@typescript-eslint/method-signature-style': 'off', 73 | /** 74 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/naming-convention.md 75 | */ 76 | '@typescript-eslint/naming-convention': [ 77 | 'off', 78 | { 79 | selector: 'default', 80 | format: ['camelCase'], 81 | leadingUnderscore: 'forbid', 82 | trailingUnderscore: 'forbid', 83 | }, 84 | { 85 | selector: 'variable', 86 | format: ['camelCase', 'UPPER_CASE'], 87 | leadingUnderscore: 'forbid', 88 | trailingUnderscore: 'forbid', 89 | }, 90 | // Enforce that type parameters (generics) are prefixed with T or U 91 | { 92 | selector: 'typeParameter', 93 | format: ['PascalCase'], 94 | prefix: ['T', 'U'], 95 | }, 96 | // enforce boolean variables to start with proper prefix. 97 | { 98 | selector: 'variable', 99 | types: ['boolean'], 100 | format: ['PascalCase'], 101 | prefix: ['is', 'should', 'has', 'can', 'did', 'will'], 102 | }, 103 | // Enforce that interface names do not begin with an I 104 | { 105 | selector: 'interface', 106 | format: ['PascalCase'], 107 | custom: { 108 | regex: '^I[A-Z]', 109 | match: false, 110 | }, 111 | }, 112 | { 113 | selector: [ 114 | 'function', 115 | 'parameter', 116 | 'property', 117 | 'parameterProperty', 118 | 'method', 119 | 'accessor', 120 | ], 121 | format: ['camelCase'], 122 | leadingUnderscore: 'forbid', 123 | trailingUnderscore: 'forbid', 124 | }, 125 | { 126 | selector: ['class', 'interface', 'typeAlias', 'enum', 'typeParameter'], 127 | format: ['PascalCase'], 128 | leadingUnderscore: 'forbid', 129 | trailingUnderscore: 'forbid', 130 | }, 131 | ], 132 | /** 133 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-base-to-string.md 134 | */ 135 | '@typescript-eslint/no-base-to-string': 'off', 136 | /** 137 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-confusing-non-null-assertion.md 138 | */ 139 | '@typescript-eslint/no-confusing-non-null-assertion': 'error', 140 | /** 141 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-dynamic-delete.md 142 | */ 143 | '@typescript-eslint/no-dynamic-delete': 'error', 144 | /** 145 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-empty-interface.md 146 | */ 147 | '@typescript-eslint/no-empty-interface': 'off', 148 | /** 149 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md 150 | */ 151 | '@typescript-eslint/no-explicit-any': [ 152 | 'warn', 153 | { 154 | ignoreRestArgs: true, 155 | // enable later 156 | fixToUnknown: false, 157 | }, 158 | ], 159 | /** 160 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-extra-non-null-assertion.md 161 | */ 162 | '@typescript-eslint/no-extra-non-null-assertion': 'error', 163 | /** 164 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-extraneous-class.md 165 | */ 166 | '@typescript-eslint/no-extraneous-class': [ 167 | 'error', 168 | { 169 | allowConstructorOnly: false, 170 | allowEmpty: false, 171 | allowStaticOnly: false, 172 | allowWithDecorator: false, 173 | }, 174 | ], 175 | /** 176 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-floating-promises.md 177 | */ 178 | '@typescript-eslint/no-floating-promises': 'off', 179 | /** 180 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-for-in-array.md 181 | */ 182 | '@typescript-eslint/no-for-in-array': 'off', 183 | /** 184 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-implicit-any-catch.md 185 | */ 186 | '@typescript-eslint/no-implicit-any-catch': [ 187 | 'error', 188 | { 189 | allowExplicitAny: false, 190 | }, 191 | ], 192 | /** 193 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-implied-eval.md 194 | */ 195 | '@typescript-eslint/no-implied-eval': 'off', 196 | /** 197 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-inferrable-types.md 198 | */ 199 | '@typescript-eslint/no-inferrable-types': [ 200 | 'error', 201 | { 202 | ignoreParameters: false, 203 | ignoreProperties: false, 204 | }, 205 | ], 206 | /** 207 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-invalid-void-type.md 208 | */ 209 | '@typescript-eslint/no-invalid-void-type': [ 210 | 'off', 211 | { 212 | allowInGenericTypeArguments: true, 213 | }, 214 | ], 215 | /** 216 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-new.md 217 | */ 218 | '@typescript-eslint/no-misused-new': 'error', 219 | /** 220 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-promises.md 221 | */ 222 | '@typescript-eslint/no-misused-promises': 'off', 223 | /** 224 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-namespace.md 225 | */ 226 | '@typescript-eslint/no-namespace': 'off', 227 | /** 228 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-non-null-asserted-optional-chain.md 229 | */ 230 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', 231 | /** 232 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-non-null-assertion.md 233 | */ 234 | '@typescript-eslint/no-non-null-assertion': 'warn', 235 | /** 236 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-parameter-properties.md 237 | */ 238 | '@typescript-eslint/no-parameter-properties': 'error', 239 | /** 240 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-require-imports.md 241 | */ 242 | '@typescript-eslint/no-require-imports': 'error', 243 | /** 244 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-this-alias.md 245 | */ 246 | '@typescript-eslint/no-this-alias': 'error', 247 | /** 248 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-throw-literal.md 249 | */ 250 | '@typescript-eslint/no-throw-literal': 'off', 251 | /** 252 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-type-alias.md 253 | */ 254 | '@typescript-eslint/no-type-alias': [ 255 | 'off', 256 | { 257 | allowAliases: 'always', 258 | allowCallbacks: 'always', 259 | allowConditionalTypes: 'always', 260 | allowConstructors: 'never', 261 | allowLiterals: 'in-unions-and-intersections', 262 | allowMappedTypes: 'always', 263 | allowTupleTypes: 'always', 264 | }, 265 | ], 266 | /** 267 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.md 268 | */ 269 | '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off', 270 | /** 271 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md 272 | */ 273 | '@typescript-eslint/no-unnecessary-condition': 'off', 274 | /** 275 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-qualifier.md 276 | */ 277 | '@typescript-eslint/no-unnecessary-qualifier': 'off', 278 | /** 279 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-type-arguments.md 280 | */ 281 | '@typescript-eslint/no-unnecessary-type-arguments': 'off', 282 | /** 283 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.md 284 | */ 285 | '@typescript-eslint/no-unnecessary-type-assertion': 'off', 286 | /** 287 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unsafe-assignment.md 288 | */ 289 | '@typescript-eslint/no-unsafe-assignment': 'off', 290 | /** 291 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unsafe-call.md 292 | */ 293 | '@typescript-eslint/no-unsafe-call': 'off', 294 | /** 295 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unsafe-member-access.md 296 | */ 297 | '@typescript-eslint/no-unsafe-member-access': 'off', 298 | /** 299 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unsafe-return.md 300 | */ 301 | '@typescript-eslint/no-unsafe-return': 'off', 302 | /** 303 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-var-requires.md 304 | */ 305 | '@typescript-eslint/no-var-requires': 'error', 306 | /** 307 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-as-const.md 308 | */ 309 | '@typescript-eslint/prefer-as-const': 'error', 310 | /** 311 | * We don't care about enums having implicit values. 312 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-enum-initializers.md 313 | */ 314 | '@typescript-eslint/prefer-enum-initializers': 'off', 315 | /** 316 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-for-of.md 317 | */ 318 | '@typescript-eslint/prefer-for-of': 'error', 319 | /** 320 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-includes.md 321 | */ 322 | '@typescript-eslint/prefer-includes': 'off', 323 | /** 324 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-literal-enum-member.md 325 | */ 326 | '@typescript-eslint/prefer-literal-enum-member': 'error', 327 | /** 328 | * using ES2015 syntax so this rule can be safetly turned off 329 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-namespace-keyword.md 330 | */ 331 | '@typescript-eslint/prefer-namespace-keyword': 'off', 332 | /** 333 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md 334 | */ 335 | '@typescript-eslint/prefer-nullish-coalescing': 'off', 336 | /** 337 | * only set to warn because there are some cases this behavior doesnt work because 338 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-optional-chain.md 339 | */ 340 | '@typescript-eslint/prefer-optional-chain': 'warn', 341 | /** 342 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-readonly.md 343 | */ 344 | '@typescript-eslint/prefer-readonly': 'off', 345 | /** 346 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md 347 | */ 348 | '@typescript-eslint/prefer-readonly-parameter-types': 'off', 349 | /** 350 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-reduce-type-parameter.md 351 | */ 352 | '@typescript-eslint/prefer-reduce-type-parameter': 'off', 353 | /** 354 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-regexp-exec.md 355 | */ 356 | '@typescript-eslint/prefer-regexp-exec': 'off', 357 | /** 358 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md 359 | */ 360 | '@typescript-eslint/prefer-string-starts-ends-with': 'off', 361 | /** 362 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-ts-expect-error.md 363 | */ 364 | '@typescript-eslint/prefer-ts-expect-error': 'warn', 365 | /** 366 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/promise-function-async.md 367 | */ 368 | '@typescript-eslint/promise-function-async': 'off', 369 | /** 370 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/require-array-sort-compare.md 371 | */ 372 | '@typescript-eslint/require-array-sort-compare': 'off', 373 | /** 374 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/restrict-plus-operands.md 375 | */ 376 | '@typescript-eslint/restrict-plus-operands': 'off', 377 | /** 378 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/restrict-template-expressions.md 379 | */ 380 | '@typescript-eslint/restrict-template-expressions': 'off', 381 | /** 382 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/strict-boolean-expressions.md 383 | */ 384 | '@typescript-eslint/strict-boolean-expressions': 'off', 385 | /** 386 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md 387 | */ 388 | '@typescript-eslint/switch-exhaustiveness-check': 'off', 389 | /** 390 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/triple-slash-reference.md 391 | */ 392 | '@typescript-eslint/triple-slash-reference': 'error', 393 | /** 394 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/type-annotation-spacing.md 395 | */ 396 | '@typescript-eslint/type-annotation-spacing': [ 397 | 'error', 398 | { 399 | before: false, 400 | after: true, 401 | overrides: { 402 | arrow: { 403 | before: true, 404 | after: true, 405 | }, 406 | }, 407 | }, 408 | ], 409 | /** 410 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/typedef.md 411 | */ 412 | '@typescript-eslint/typedef': [ 413 | 'error', 414 | { 415 | arrayDestructuring: false, 416 | arrowParameter: false, 417 | memberVariableDeclaration: false, 418 | objectDestructuring: false, 419 | parameter: false, 420 | propertyDeclaration: true, 421 | variableDeclaration: false, 422 | variableDeclarationIgnoreFunction: false, 423 | }, 424 | ], 425 | /** 426 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/unbound-method.md 427 | */ 428 | '@typescript-eslint/unbound-method': 'off', 429 | /** 430 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/unified-signatures.md 431 | */ 432 | '@typescript-eslint/unified-signatures': 'off', 433 | 434 | // @typescript-eslint Extension Rules 435 | // ================================================================================== 436 | /** 437 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/brace-style.md 438 | */ 439 | 'brace-style': 'off', 440 | '@typescript-eslint/brace-style': 'error', 441 | /** 442 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/comma-spacing.md 443 | */ 444 | 'comma-spacing': 'off', 445 | '@typescript-eslint/comma-spacing': 'error', 446 | /** 447 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/default-param-last.md 448 | */ 449 | 'default-param-last': 'off', 450 | '@typescript-eslint/default-param-last': 'error', 451 | /** 452 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/dot-notation.md 453 | */ 454 | 'dot-notation': 'error', 455 | '@typescript-eslint/dot-notation': 'off', 456 | /** 457 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/func-call-spacing.md 458 | */ 459 | 'func-call-spacing': 'off', 460 | '@typescript-eslint/func-call-spacing': 'error', 461 | /** 462 | * use prettier instead 463 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/indent.md 464 | */ 465 | indent: 'off', 466 | '@typescript-eslint/indent': 'off', 467 | /** 468 | * Allow a mix between the two 469 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/init-declarations.md 470 | */ 471 | '@typescript-eslint/init-declarations': 'off', 472 | /** 473 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/keyword-spacing.md 474 | */ 475 | 'keyword-spacing': 'off', 476 | '@typescript-eslint/keyword-spacing': 'error', 477 | /** 478 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/lines-between-class-members.md 479 | */ 480 | 'lines-between-class-members': 'off', 481 | '@typescript-eslint/lines-between-class-members': [ 482 | 'error', 483 | 'always', 484 | { 485 | // base eslint config 486 | exceptAfterSingleLine: true, 487 | // typescript specific 488 | exceptAfterOverload: true, 489 | }, 490 | ], 491 | /** 492 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-array-constructor.md 493 | */ 494 | 'no-array-constructor': 'off', 495 | '@typescript-eslint/no-array-constructor': 'error', 496 | /** 497 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-dupe-class-members.md 498 | */ 499 | 'no-dupe-class-members': 'off', 500 | '@typescript-eslint/no-dupe-class-members': 'error', 501 | /** 502 | * Use prettier 503 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-extra-parens.md 504 | */ 505 | 'no-extra-parens': 'off', 506 | '@typescript-eslint/no-extra-parens': 'off', 507 | /** 508 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-extra-semi.md 509 | */ 510 | 'no-extra-semi': 'off', 511 | '@typescript-eslint/no-extra-semi': 'error', 512 | /** 513 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-invalid-this.md 514 | */ 515 | 'no-invalid-this': 'off', 516 | '@typescript-eslint/no-invalid-this': 'error', 517 | /** 518 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-loss-of-precision.md 519 | */ 520 | 'no-loss-of-precision': 'off', 521 | '@typescript-eslint/no-loss-of-precision': 'error', 522 | /** 523 | * https://eslint.org/docs/rules/no-magic-numbers 524 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-magic-numbers.md 525 | */ 526 | 'no-magic-numbers': 'off', 527 | '@typescript-eslint/no-magic-numbers': [ 528 | 'off', 529 | { 530 | // base eslint configs 531 | ignoreArrayIndexes: true, 532 | ignoreDefaultValues: true, 533 | enforceConst: true, 534 | // typescript specific configs 535 | ignoreEnums: true, 536 | ignoreNumericLiteralTypes: true, 537 | ignoreReadonlyClassProperties: true, 538 | }, 539 | ], 540 | /** 541 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-redeclare.md 542 | */ 543 | 'no-redeclare': 'off', 544 | '@typescript-eslint/no-redeclare': [ 545 | 'error', 546 | { 547 | // prevents variables from being created with global variable naming 548 | builtinGlobals: true, 549 | }, 550 | ], 551 | /** 552 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-shadow.md 553 | */ 554 | 'no-shadow': 'off', 555 | '@typescript-eslint/no-shadow': [ 556 | 'error', 557 | { 558 | // No variables + types with same naming 559 | ignoreTypeValueShadow: false, 560 | }, 561 | ], 562 | /** 563 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-expressions.md 564 | */ 565 | 'no-unused-expressions': 'off', 566 | '@typescript-eslint/no-unused-expressions': 'error', 567 | /** 568 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md 569 | */ 570 | 'no-unused-vars': 'off', 571 | '@typescript-eslint/no-unused-vars': [ 572 | 'error', 573 | { 574 | ignoreRestSiblings: true, 575 | }, 576 | ], 577 | /** 578 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md 579 | */ 580 | 'no-use-before-define': 'off', 581 | '@typescript-eslint/no-use-before-define': 'off', 582 | /** 583 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-useless-constructor.md 584 | */ 585 | 'no-useless-constructor': 'off', 586 | '@typescript-eslint/no-useless-constructor': 'error', 587 | /** 588 | * https://eslint.org/docs/rules/quotes 589 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/quotes.md 590 | */ 591 | quotes: 'off', 592 | '@typescript-eslint/quotes': [ 593 | 'error', 594 | 'single', 595 | { 596 | avoidEscape: true, 597 | allowTemplateLiterals: true, 598 | }, 599 | ], 600 | /** 601 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/require-await.md 602 | */ 603 | '@typescript-eslint/require-await': 'off', 604 | /** 605 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/return-await.md 606 | */ 607 | '@typescript-eslint/return-await': 'off', 608 | /** 609 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/semi.md 610 | */ 611 | semi: 'off', 612 | '@typescript-eslint/semi': ['error', 'never'], 613 | /** 614 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/space-before-function-paren.md 615 | */ 616 | 'space-before-function-paren': 'off', 617 | '@typescript-eslint/space-before-function-paren': [ 618 | 'error', 619 | { 620 | anonymous: 'never', 621 | named: 'never', 622 | asyncArrow: 'always', 623 | }, 624 | ], 625 | }, 626 | }, 627 | ], 628 | } 629 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-ai", 3 | "version": "0.0.71", 4 | "homepage:": "https://polyxo.de", 5 | "repository": "git@github.com:payloadcms/payload-plugin-template.git", 6 | "description": "Payload AI tools", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "keywords": [ 10 | "payload", 11 | "cms", 12 | "plugin", 13 | "typescript", 14 | "react", 15 | "template" 16 | ], 17 | "scripts": { 18 | "build": "tsc && yarn copyfiles", 19 | "test": "cd dev && yarn test", 20 | "lint": "eslint src", 21 | "yalc:deploy": "yarn build && yalc publish --push", 22 | "lint:fix": "eslint --fix --ext .ts,.tsx src", 23 | "clean": "rimraf dist && rimraf dev/yarn.lock", 24 | "prepublishOnly": "yarn clean && yarn build && cd dev", 25 | "copyfiles": "copyfiles -u 1 \"./src/**/*/*.scss\" dist" 26 | }, 27 | "author": "robert@wirewire.de", 28 | "license": "MIT", 29 | "peerDependencies": { 30 | "payload": "^2.0.0" 31 | }, 32 | "devDependencies": { 33 | "@payloadcms/eslint-config": "^0.0.1", 34 | "@swc/jest": "^0.2.28", 35 | "@types/jest": "^29.5.11", 36 | "@typescript-eslint/eslint-plugin": "5.12.1", 37 | "@typescript-eslint/parser": "5.12.1", 38 | "copyfiles": "^2.4.1", 39 | "dotenv": "^8.2.0", 40 | "eslint": "^8.19.0", 41 | "eslint-config-airbnb-base": "^14.2.1", 42 | "eslint-config-prettier": "^8.5.0", 43 | "eslint-plugin-import": "2.25.4", 44 | "eslint-plugin-prettier": "^4.0.0", 45 | "jest": "^29.7.0", 46 | "payload": "^2.6.0", 47 | "prettier": "^2.7.1", 48 | "react": "^18.0.0", 49 | "typescript": "^4.8.4" 50 | }, 51 | "dependencies": { 52 | "@faceless-ui/modal": "2.0.1", 53 | "axios": "^1.6.8", 54 | "iso-639-1": "^3.1.2", 55 | "node-fetch": "^3.3.2", 56 | "openai": "^4.46.1", 57 | "webpack": "^5.89.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/access/admins.ts: -------------------------------------------------------------------------------- 1 | import type { AccessArgs } from 'payload/config' 2 | 3 | import { checkRole } from './checkRole' 4 | //import type { User } from '../payload-types' 5 | 6 | type isAdmin = (args: AccessArgs) => boolean 7 | 8 | export const admins: isAdmin = ({ req: { user } }) => { 9 | if (!user) return false 10 | return checkRole(['admin'], user) 11 | } 12 | -------------------------------------------------------------------------------- /src/access/adminsOrPublished.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from 'payload/config' 2 | 3 | import { checkRole } from './checkRole' 4 | 5 | export const adminsOrPublished: Access = ({ req: { user } }) => { 6 | if (user && checkRole(['admin'], user)) { 7 | return true 8 | } 9 | 10 | return true 11 | return { 12 | _status: { 13 | equals: 'published', 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/access/anyone.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from 'payload/config' 2 | 3 | export const anyone: Access = () => true 4 | -------------------------------------------------------------------------------- /src/access/checkRole.ts: -------------------------------------------------------------------------------- 1 | //import type { User } from '../../payload-types' 2 | 3 | export const checkRole = (allRoles: any /* User['roles'] = [] */, user?: any): boolean => { 4 | if (user) { 5 | if ( 6 | allRoles.some((role: any) => { 7 | return user?.roles?.some((individualRole: any) => { 8 | return individualRole === role 9 | }) 10 | }) 11 | ) 12 | return true 13 | } 14 | 15 | return false 16 | } 17 | -------------------------------------------------------------------------------- /src/access/validateAccess.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from 'payload/config' 2 | 3 | export const validateAccess = (req: any, res: any, pluginOptions: any) => { 4 | const collectionOptions = pluginOptions.collections[req.collection.config.slug] 5 | 6 | const accessControl = collectionOptions.access || req.collection.config.access.update 7 | const access = accessControl({ req }) 8 | 9 | if (!access) { 10 | res.status(403).send({ error: 'not allowed' }) 11 | } 12 | return access 13 | } 14 | -------------------------------------------------------------------------------- /src/aiCaption.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import OpenAI from 'openai' 3 | import type { CollectionAfterChangeHook, FieldHook } from 'payload/types' 4 | 5 | const aiCaptionHook = 6 | ( 7 | { 8 | collectionOptions, 9 | collection, 10 | pluginOptions, 11 | }: { collectionOptions: any; collection: object; pluginOptions: any }, 12 | fallback?: string, 13 | ): CollectionAfterChangeHook => 14 | async ({ doc, req, previousDoc, context, collection }) => { 15 | const settings = pluginOptions.collections?.[collection.slug]?.settings 16 | 17 | return await translateCollection({ 18 | doc, 19 | req, 20 | previousDoc, 21 | context, 22 | collection, 23 | collectionOptions, 24 | settings, 25 | }) 26 | } 27 | 28 | export default aiCaptionHook 29 | 30 | async function processImageRequest(url: string) { 31 | const response = await axios.get(url, { responseType: 'arraybuffer' }) 32 | const imageBuffer = Buffer.from(response.data, 'binary') 33 | const base64Image = imageBuffer.toString('base64') 34 | const mimeType = response.headers['content-type'] 35 | const encodedImage = `data:${mimeType};base64,${base64Image}` 36 | 37 | const openai = new OpenAI({ 38 | apiKey: process.env.OPENAI_API_KEY, 39 | }) 40 | 41 | const responseGpt = await openai.chat.completions.create({ 42 | model: 'gpt-4o', 43 | messages: [ 44 | { 45 | role: 'user', 46 | content: [ 47 | { type: 'text', text: 'Create an image alt text' }, 48 | { type: 'image_url', image_url: { url: encodedImage } }, 49 | ], 50 | }, 51 | ], 52 | max_tokens: 1024, 53 | }) 54 | 55 | console.log('GPT Response:', responseGpt.choices[0].message.content) 56 | return responseGpt 57 | } 58 | 59 | export async function translateCollection({ 60 | req, 61 | doc, 62 | collection, 63 | previousDoc, 64 | context, 65 | collectionOptions, 66 | onlyMissing, 67 | codes, 68 | settings, 69 | sourceLanguage, 70 | }: any) { 71 | if (context.triggerAfterChange === false /* || req.locale !== sourceLanguageI */) return 72 | console.log('upload doc', doc) 73 | if (!doc?.sizes?.tablet?.url) return 74 | const responseGpt = await processImageRequest(`http://localhost:3000${doc.sizes.tablet.url}`) 75 | 76 | const docData = doc 77 | 78 | docData.alt = responseGpt.choices[0].message.content 79 | const updatedLanguage = await req.payload.update({ 80 | //req, 81 | collection: collection.slug, 82 | id: doc.id, 83 | data: docData, 84 | limit: 1, 85 | depth: 0, 86 | context: { 87 | triggerAfterChange: false, 88 | }, 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /src/aiTranslate.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | import type { CollectionAfterChangeHook, FieldHook } from 'payload/types' 3 | import { deepCompareTranslateAndMerge } from './deepCompareAndMerge' 4 | 5 | const aiTranslateHook = 6 | ( 7 | { 8 | collectionOptions, 9 | collection, 10 | pluginOptions, 11 | }: { collectionOptions: any; collection: object; pluginOptions: any }, 12 | fallback?: string, 13 | ): CollectionAfterChangeHook => 14 | async ({ doc, req, previousDoc, context, collection }) => { 15 | const settings = pluginOptions.collections?.[collection.slug]?.settings 16 | 17 | return await translateCollection({ 18 | doc, 19 | req, 20 | previousDoc, 21 | context, 22 | collection, 23 | collectionOptions, 24 | settings, 25 | }) 26 | } 27 | 28 | export default aiTranslateHook 29 | 30 | export async function translateCollection({ 31 | req, 32 | doc, 33 | collection, 34 | previousDoc, 35 | context, 36 | collectionOptions, 37 | onlyMissing, 38 | codes, 39 | settings, 40 | sourceLanguage, 41 | }: any) { 42 | const sourceLanguageI = 43 | sourceLanguage || doc.sourceLanguage || req.payload.config.localization.defaultLocale 44 | 45 | if (context.triggerAfterChange === false /* || req.locale !== sourceLanguageI */) return 46 | 47 | const localCodes: string[] = req.payload.config.localization.localeCodes 48 | 49 | const translationPromises = localCodes 50 | .filter( 51 | targetLanguage => 52 | targetLanguage !== sourceLanguageI && (!codes || codes.includes(targetLanguage)), 53 | ) 54 | .map(async (tL: string) => { 55 | const targetDoc = await req.payload.findByID({ 56 | collection: collection.slug, 57 | id: doc.id, 58 | locale: tL, 59 | fallbackLocale: false, 60 | limit: 0, 61 | depth: 0, 62 | }) 63 | 64 | const targetDocWithTranslation = await deepCompareTranslateAndMerge( 65 | doc, 66 | previousDoc, 67 | targetDoc, 68 | collectionOptions.fields, 69 | tL, 70 | previousDoc.id ? 'update' : 'create', 71 | onlyMissing, 72 | sourceLanguageI, 73 | { ...settings, namespace: doc?.namespace, localization: req.payload.config.localization }, 74 | ) 75 | 76 | const { id, _status, updatedAt, createdAt, publishedDate, ...dataNew } = 77 | targetDocWithTranslation 78 | 79 | return { dataNew, tL } 80 | }) 81 | 82 | console.log('translationPromises', translationPromises) 83 | 84 | const translationResults = await Promise.all(translationPromises) 85 | 86 | for (const translatedContent of translationResults) { 87 | const updatedLanguage = await req.payload.update({ 88 | //req, 89 | collection: collection.slug, 90 | id: doc.id, 91 | data: translatedContent.dataNew, 92 | locale: translatedContent.tL, 93 | limit: 1, 94 | depth: 0, 95 | context: { 96 | triggerAfterChange: false, 97 | }, 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/components/AfterDashboard/index.scss: -------------------------------------------------------------------------------- 1 | .after-dashboard { 2 | background-color: var(--theme-success-200); 3 | border: 1px solid var(--theme-success-300); 4 | color: var(--theme-success-500); 5 | padding: var(--base); 6 | 7 | p { 8 | margin-bottom: 0; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/AfterDashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // import './index.scss'; 4 | 5 | const baseClass = 'after-dashboard' 6 | 7 | const AfterDashboard: React.FC = () => { 8 | return ( 9 |
10 |

This component was added by the plugin

11 |
12 | Find it here: src/components/afterDashboard 13 |
14 |
15 | ) 16 | } 17 | 18 | export default AfterDashboard 19 | -------------------------------------------------------------------------------- /src/components/Metadata/index.scss: -------------------------------------------------------------------------------- 1 | .after-dashboard { 2 | background-color: var(--theme-success-200); 3 | border: 1px solid var(--theme-success-300); 4 | color: var(--theme-success-500); 5 | padding: var(--base); 6 | 7 | p { 8 | margin-bottom: 0; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Metadata/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Button } from 'payload/components/elements' 4 | import { useDocumentInfo, useLocale } from 'payload/components/utilities' 5 | import { useAllFormFields, reduceFieldsToValues, useForm } from 'payload/components/forms' 6 | import { Fields } from 'payload/dist/admin/components/forms/Form/types' 7 | 8 | const baseClass = 'after-dashboard' 9 | 10 | export const GenerateMetadata: React.FC = () => { 11 | const [isLoading, setIsLoading] = React.useState(false) 12 | 13 | const [fields, dispatchFields] = useAllFormFields() 14 | const { setModified } = useForm() 15 | 16 | const locale = useLocale() 17 | const documentInfo: any = useDocumentInfo() 18 | const translate = async () => { 19 | setIsLoading(true) 20 | try { 21 | console.log('fields', fields) 22 | dispatchFields({ 23 | type: 'REPLACE_STATE', 24 | state: { 25 | ...fields, 26 | title: { ...fields.title, value: 'new title here' }, 27 | } as Fields, 28 | }) 29 | 30 | setModified(true) 31 | 32 | /*const response = await fetch(`/api/${documentInfo.collection.slug}/translate`, { 33 | method: 'POST', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | }, 37 | body: JSON.stringify({ 38 | id: documentInfo.id, 39 | }), 40 | }) 41 | 42 | const translatedValues = await response.json()*/ 43 | 44 | setIsLoading(false) 45 | } catch (error) { 46 | setIsLoading(false) 47 | console.error(error) 48 | } 49 | } 50 | 51 | return ( 52 |
53 | 57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Translator/Translator.scss: -------------------------------------------------------------------------------- 1 | .ai-translator__translation-buttons { 2 | display: flex; 3 | .btn { 4 | margin-right: 1em; 5 | } 6 | } -------------------------------------------------------------------------------- /src/components/Translator/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | 3 | import { Button, Drawer, DrawerToggler } from 'payload/components/elements' 4 | import { useConfig, useDocumentInfo, useLocale } from 'payload/components/utilities' 5 | import { useModal } from '@faceless-ui/modal' 6 | import './Translator.scss' 7 | import { SelectInput, useForm } from 'payload/components/forms' 8 | import { Config } from 'payload/config' 9 | 10 | const baseClass = 'after-dashboard' 11 | 12 | // React.FC 13 | 14 | /* 15 | interface DrawerTogglerAltTypes = { 16 | children: React.ReactNode; 17 | className?: string; 18 | disabled?: boolean; 19 | onClick?: (e: any) => void; 20 | slug: string; 21 | } */ 22 | 23 | export const DrawerTogglerAlt: any = ({ 24 | children, 25 | className, 26 | disabled, 27 | onClick, 28 | slug, 29 | ...rest 30 | }: any) => { 31 | const { openModal } = useModal() 32 | console.log('openModal', openModal) 33 | 34 | const handleClick = useCallback( 35 | (e: any) => { 36 | openModal(slug) 37 | if (typeof onClick === 'function') onClick(e) 38 | }, 39 | [openModal, slug, onClick], 40 | ) 41 | 42 | return ( 43 | 46 | ) 47 | } 48 | 49 | export const Translator: React.FC = () => { 50 | const baseClass = 'ai-translator' 51 | 52 | //const { fields, getDataByPath } = useForm() 53 | 54 | const [isLoading, setIsLoading] = React.useState(false) 55 | const [selectedModel, setSelectedModel] = React.useState('default') 56 | 57 | const locale = useLocale() 58 | const config: any = useConfig() 59 | 60 | const [selectedSourceLocale, setSelectedSourceLocal] = React.useState( 61 | config.localization.defaultLocale, 62 | ) 63 | const documentInfo: any = useDocumentInfo() 64 | const translate = async ({ codes }: any) => { 65 | const settings = { 66 | model: selectedModel === 'default' ? undefined : selectedModel, 67 | } 68 | setIsLoading(true) 69 | try { 70 | const response = await fetch( 71 | `/api/${documentInfo.collection.slug}/translate?locale=${locale.code}`, 72 | { 73 | method: 'POST', 74 | headers: { 75 | 'Content-Type': 'application/json', 76 | }, 77 | body: JSON.stringify({ 78 | id: documentInfo.id, 79 | locale: selectedSourceLocale, 80 | codes, 81 | settings, 82 | }), 83 | }, 84 | ) 85 | 86 | const translatedValues = await response.json() 87 | 88 | setIsLoading(false) 89 | } catch (error) { 90 | setIsLoading(false) 91 | console.error(error) 92 | } 93 | } 94 | 95 | const { openModal } = useModal() 96 | const slug = 'ai-translator' 97 | 98 | const handleClick = useCallback( 99 | (e: any) => { 100 | console.log('handleClick') 101 | openModal(slug) 102 | // if (typeof onClick === 'function') onClick(e) 103 | }, 104 | [openModal, slug /*onClick */], 105 | ) 106 | /* 107 | 108 | */ 109 | 110 | const options = [ 111 | { 112 | label: 'Default', 113 | value: 'default', 114 | }, 115 | { 116 | label: 'GPT-4o', 117 | value: 'gpt-4o', 118 | }, 119 | { 120 | label: 'GPT-3.5 Turbo (1106)', 121 | value: 'gpt-3.5-turbo-1106', 122 | }, 123 | { 124 | label: 'GPT-4 Turbo (Preview)', 125 | value: 'gpt-4-turbo-preview', 126 | }, 127 | 128 | { 129 | label: 'GPT-4 (most expensive)', 130 | value: 'gpt-4', 131 | }, 132 | ] 133 | 134 | const optionsLocales = config.localization.locales.map((locale: any) => ({ 135 | label: locale.label, 136 | value: locale.code, 137 | })) 138 | 139 | return ( 140 |
141 | 142 | Translator 143 | 144 | 145 | 146 | {isLoading ? ( 147 | 148 | ) : ( 149 | <> 150 |
151 | { 153 | setSelectedModel(e.value) 154 | }} 155 | name="selectedModel" 156 | value={selectedModel} 157 | path="model" 158 | options={options} 159 | /> 160 |
161 |
162 | Translates from: 163 | { 165 | setSelectedSourceLocal(e.value) 166 | }} 167 | name="sourceLocale" 168 | value={selectedSourceLocale} 169 | path="sourceLocale" 170 | options={optionsLocales} 171 | /> 172 |
173 |
174 | 177 | 180 |
181 | 182 | )} 183 |
184 |
185 | ) 186 | } 187 | -------------------------------------------------------------------------------- /src/countries.json: -------------------------------------------------------------------------------- 1 | [{"id":4,"alpha2":"af","alpha3":"afg","name":"Afghanistan"}, 2 | {"id":8,"alpha2":"al","alpha3":"alb","name":"Albania"}, 3 | {"id":12,"alpha2":"dz","alpha3":"dza","name":"Algeria"}, 4 | {"id":20,"alpha2":"ad","alpha3":"and","name":"Andorra"}, 5 | {"id":24,"alpha2":"ao","alpha3":"ago","name":"Angola"}, 6 | {"id":28,"alpha2":"ag","alpha3":"atg","name":"Antigua and Barbuda"}, 7 | {"id":32,"alpha2":"ar","alpha3":"arg","name":"Argentina"}, 8 | {"id":51,"alpha2":"am","alpha3":"arm","name":"Armenia"}, 9 | {"id":36,"alpha2":"au","alpha3":"aus","name":"Australia"}, 10 | {"id":40,"alpha2":"at","alpha3":"aut","name":"Austria"}, 11 | {"id":31,"alpha2":"az","alpha3":"aze","name":"Azerbaijan"}, 12 | {"id":44,"alpha2":"bs","alpha3":"bhs","name":"Bahamas"}, 13 | {"id":48,"alpha2":"bh","alpha3":"bhr","name":"Bahrain"}, 14 | {"id":50,"alpha2":"bd","alpha3":"bgd","name":"Bangladesh"}, 15 | {"id":52,"alpha2":"bb","alpha3":"brb","name":"Barbados"}, 16 | {"id":112,"alpha2":"by","alpha3":"blr","name":"Belarus"}, 17 | {"id":56,"alpha2":"be","alpha3":"bel","name":"Belgium"}, 18 | {"id":84,"alpha2":"bz","alpha3":"blz","name":"Belize"}, 19 | {"id":204,"alpha2":"bj","alpha3":"ben","name":"Benin"}, 20 | {"id":64,"alpha2":"bt","alpha3":"btn","name":"Bhutan"}, 21 | {"id":68,"alpha2":"bo","alpha3":"bol","name":"Bolivia (Plurinational State of)"}, 22 | {"id":70,"alpha2":"ba","alpha3":"bih","name":"Bosnia and Herzegovina"}, 23 | {"id":72,"alpha2":"bw","alpha3":"bwa","name":"Botswana"}, 24 | {"id":76,"alpha2":"br","alpha3":"bra","name":"Brazil"}, 25 | {"id":96,"alpha2":"bn","alpha3":"brn","name":"Brunei Darussalam"}, 26 | {"id":100,"alpha2":"bg","alpha3":"bgr","name":"Bulgaria"}, 27 | {"id":854,"alpha2":"bf","alpha3":"bfa","name":"Burkina Faso"}, 28 | {"id":108,"alpha2":"bi","alpha3":"bdi","name":"Burundi"}, 29 | {"id":132,"alpha2":"cv","alpha3":"cpv","name":"Cabo Verde"}, 30 | {"id":116,"alpha2":"kh","alpha3":"khm","name":"Cambodia"}, 31 | {"id":120,"alpha2":"cm","alpha3":"cmr","name":"Cameroon"}, 32 | {"id":124,"alpha2":"ca","alpha3":"can","name":"Canada"}, 33 | {"id":140,"alpha2":"cf","alpha3":"caf","name":"Central African Republic"}, 34 | {"id":148,"alpha2":"td","alpha3":"tcd","name":"Chad"}, 35 | {"id":152,"alpha2":"cl","alpha3":"chl","name":"Chile"}, 36 | {"id":156,"alpha2":"cn","alpha3":"chn","name":"China"}, 37 | {"id":170,"alpha2":"co","alpha3":"col","name":"Colombia"}, 38 | {"id":174,"alpha2":"km","alpha3":"com","name":"Comoros"}, 39 | {"id":178,"alpha2":"cg","alpha3":"cog","name":"Congo"}, 40 | {"id":180,"alpha2":"cd","alpha3":"cod","name":"Congo, Democratic Republic of the"}, 41 | {"id":188,"alpha2":"cr","alpha3":"cri","name":"Costa Rica"}, 42 | {"id":384,"alpha2":"ci","alpha3":"civ","name":"Côte d'Ivoire"}, 43 | {"id":191,"alpha2":"hr","alpha3":"hrv","name":"Croatia"}, 44 | {"id":192,"alpha2":"cu","alpha3":"cub","name":"Cuba"}, 45 | {"id":196,"alpha2":"cy","alpha3":"cyp","name":"Cyprus"}, 46 | {"id":203,"alpha2":"cz","alpha3":"cze","name":"Czechia"}, 47 | {"id":208,"alpha2":"dk","alpha3":"dnk","name":"Denmark"}, 48 | {"id":262,"alpha2":"dj","alpha3":"dji","name":"Djibouti"}, 49 | {"id":212,"alpha2":"dm","alpha3":"dma","name":"Dominica"}, 50 | {"id":214,"alpha2":"do","alpha3":"dom","name":"Dominican Republic"}, 51 | {"id":218,"alpha2":"ec","alpha3":"ecu","name":"Ecuador"}, 52 | {"id":818,"alpha2":"eg","alpha3":"egy","name":"Egypt"}, 53 | {"id":222,"alpha2":"sv","alpha3":"slv","name":"El Salvador"}, 54 | {"id":226,"alpha2":"gq","alpha3":"gnq","name":"Equatorial Guinea"}, 55 | {"id":232,"alpha2":"er","alpha3":"eri","name":"Eritrea"}, 56 | {"id":233,"alpha2":"ee","alpha3":"est","name":"Estonia"}, 57 | {"id":748,"alpha2":"sz","alpha3":"swz","name":"Eswatini"}, 58 | {"id":231,"alpha2":"et","alpha3":"eth","name":"Ethiopia"}, 59 | {"id":242,"alpha2":"fj","alpha3":"fji","name":"Fiji"}, 60 | {"id":246,"alpha2":"fi","alpha3":"fin","name":"Finland"}, 61 | {"id":250,"alpha2":"fr","alpha3":"fra","name":"France"}, 62 | {"id":266,"alpha2":"ga","alpha3":"gab","name":"Gabon"}, 63 | {"id":270,"alpha2":"gm","alpha3":"gmb","name":"Gambia"}, 64 | {"id":268,"alpha2":"ge","alpha3":"geo","name":"Georgia"}, 65 | {"id":276,"alpha2":"de","alpha3":"deu","name":"Germany"}, 66 | {"id":288,"alpha2":"gh","alpha3":"gha","name":"Ghana"}, 67 | {"id":300,"alpha2":"gr","alpha3":"grc","name":"Greece"}, 68 | {"id":308,"alpha2":"gd","alpha3":"grd","name":"Grenada"}, 69 | {"id":320,"alpha2":"gt","alpha3":"gtm","name":"Guatemala"}, 70 | {"id":324,"alpha2":"gn","alpha3":"gin","name":"Guinea"}, 71 | {"id":624,"alpha2":"gw","alpha3":"gnb","name":"Guinea-Bissau"}, 72 | {"id":328,"alpha2":"gy","alpha3":"guy","name":"Guyana"}, 73 | {"id":332,"alpha2":"ht","alpha3":"hti","name":"Haiti"}, 74 | {"id":340,"alpha2":"hn","alpha3":"hnd","name":"Honduras"}, 75 | {"id":348,"alpha2":"hu","alpha3":"hun","name":"Hungary"}, 76 | {"id":352,"alpha2":"is","alpha3":"isl","name":"Iceland"}, 77 | {"id":356,"alpha2":"in","alpha3":"ind","name":"India"}, 78 | {"id":360,"alpha2":"id","alpha3":"idn","name":"Indonesia"}, 79 | {"id":364,"alpha2":"ir","alpha3":"irn","name":"Iran (Islamic Republic of)"}, 80 | {"id":368,"alpha2":"iq","alpha3":"irq","name":"Iraq"}, 81 | {"id":372,"alpha2":"ie","alpha3":"irl","name":"Ireland"}, 82 | {"id":376,"alpha2":"il","alpha3":"isr","name":"Israel"}, 83 | {"id":380,"alpha2":"it","alpha3":"ita","name":"Italy"}, 84 | {"id":388,"alpha2":"jm","alpha3":"jam","name":"Jamaica"}, 85 | {"id":392,"alpha2":"jp","alpha3":"jpn","name":"Japan"}, 86 | {"id":400,"alpha2":"jo","alpha3":"jor","name":"Jordan"}, 87 | {"id":398,"alpha2":"kz","alpha3":"kaz","name":"Kazakhstan"}, 88 | {"id":404,"alpha2":"ke","alpha3":"ken","name":"Kenya"}, 89 | {"id":296,"alpha2":"ki","alpha3":"kir","name":"Kiribati"}, 90 | {"id":408,"alpha2":"kp","alpha3":"prk","name":"Korea (Democratic People's Republic of)"}, 91 | {"id":410,"alpha2":"kr","alpha3":"kor","name":"Korea, Republic of"}, 92 | {"id":414,"alpha2":"kw","alpha3":"kwt","name":"Kuwait"}, 93 | {"id":417,"alpha2":"kg","alpha3":"kgz","name":"Kyrgyzstan"}, 94 | {"id":418,"alpha2":"la","alpha3":"lao","name":"Lao People's Democratic Republic"}, 95 | {"id":428,"alpha2":"lv","alpha3":"lva","name":"Latvia"}, 96 | {"id":422,"alpha2":"lb","alpha3":"lbn","name":"Lebanon"}, 97 | {"id":426,"alpha2":"ls","alpha3":"lso","name":"Lesotho"}, 98 | {"id":430,"alpha2":"lr","alpha3":"lbr","name":"Liberia"}, 99 | {"id":434,"alpha2":"ly","alpha3":"lby","name":"Libya"}, 100 | {"id":438,"alpha2":"li","alpha3":"lie","name":"Liechtenstein"}, 101 | {"id":440,"alpha2":"lt","alpha3":"ltu","name":"Lithuania"}, 102 | {"id":442,"alpha2":"lu","alpha3":"lux","name":"Luxembourg"}, 103 | {"id":450,"alpha2":"mg","alpha3":"mdg","name":"Madagascar"}, 104 | {"id":454,"alpha2":"mw","alpha3":"mwi","name":"Malawi"}, 105 | {"id":458,"alpha2":"my","alpha3":"mys","name":"Malaysia"}, 106 | {"id":462,"alpha2":"mv","alpha3":"mdv","name":"Maldives"}, 107 | {"id":466,"alpha2":"ml","alpha3":"mli","name":"Mali"}, 108 | {"id":470,"alpha2":"mt","alpha3":"mlt","name":"Malta"}, 109 | {"id":584,"alpha2":"mh","alpha3":"mhl","name":"Marshall Islands"}, 110 | {"id":478,"alpha2":"mr","alpha3":"mrt","name":"Mauritania"}, 111 | {"id":480,"alpha2":"mu","alpha3":"mus","name":"Mauritius"}, 112 | {"id":484,"alpha2":"mx","alpha3":"mex","name":"Mexico"}, 113 | {"id":583,"alpha2":"fm","alpha3":"fsm","name":"Micronesia (Federated States of)"}, 114 | {"id":498,"alpha2":"md","alpha3":"mda","name":"Moldova, Republic of"}, 115 | {"id":492,"alpha2":"mc","alpha3":"mco","name":"Monaco"}, 116 | {"id":496,"alpha2":"mn","alpha3":"mng","name":"Mongolia"}, 117 | {"id":499,"alpha2":"me","alpha3":"mne","name":"Montenegro"}, 118 | {"id":504,"alpha2":"ma","alpha3":"mar","name":"Morocco"}, 119 | {"id":508,"alpha2":"mz","alpha3":"moz","name":"Mozambique"}, 120 | {"id":104,"alpha2":"mm","alpha3":"mmr","name":"Myanmar"}, 121 | {"id":516,"alpha2":"na","alpha3":"nam","name":"Namibia"}, 122 | {"id":520,"alpha2":"nr","alpha3":"nru","name":"Nauru"}, 123 | {"id":524,"alpha2":"np","alpha3":"npl","name":"Nepal"}, 124 | {"id":528,"alpha2":"nl","alpha3":"nld","name":"Netherlands"}, 125 | {"id":554,"alpha2":"nz","alpha3":"nzl","name":"New Zealand"}, 126 | {"id":558,"alpha2":"ni","alpha3":"nic","name":"Nicaragua"}, 127 | {"id":562,"alpha2":"ne","alpha3":"ner","name":"Niger"}, 128 | {"id":566,"alpha2":"ng","alpha3":"nga","name":"Nigeria"}, 129 | {"id":807,"alpha2":"mk","alpha3":"mkd","name":"North Macedonia"}, 130 | {"id":578,"alpha2":"no","alpha3":"nor","name":"Norway"}, 131 | {"id":512,"alpha2":"om","alpha3":"omn","name":"Oman"}, 132 | {"id":586,"alpha2":"pk","alpha3":"pak","name":"Pakistan"}, 133 | {"id":585,"alpha2":"pw","alpha3":"plw","name":"Palau"}, 134 | {"id":591,"alpha2":"pa","alpha3":"pan","name":"Panama"}, 135 | {"id":598,"alpha2":"pg","alpha3":"png","name":"Papua New Guinea"}, 136 | {"id":600,"alpha2":"py","alpha3":"pry","name":"Paraguay"}, 137 | {"id":604,"alpha2":"pe","alpha3":"per","name":"Peru"}, 138 | {"id":608,"alpha2":"ph","alpha3":"phl","name":"Philippines"}, 139 | {"id":616,"alpha2":"pl","alpha3":"pol","name":"Poland"}, 140 | {"id":620,"alpha2":"pt","alpha3":"prt","name":"Portugal"}, 141 | {"id":634,"alpha2":"qa","alpha3":"qat","name":"Qatar"}, 142 | {"id":642,"alpha2":"ro","alpha3":"rou","name":"Romania"}, 143 | {"id":643,"alpha2":"ru","alpha3":"rus","name":"Russian Federation"}, 144 | {"id":646,"alpha2":"rw","alpha3":"rwa","name":"Rwanda"}, 145 | {"id":659,"alpha2":"kn","alpha3":"kna","name":"Saint Kitts and Nevis"}, 146 | {"id":662,"alpha2":"lc","alpha3":"lca","name":"Saint Lucia"}, 147 | {"id":670,"alpha2":"vc","alpha3":"vct","name":"Saint Vincent and the Grenadines"}, 148 | {"id":882,"alpha2":"ws","alpha3":"wsm","name":"Samoa"}, 149 | {"id":674,"alpha2":"sm","alpha3":"smr","name":"San Marino"}, 150 | {"id":678,"alpha2":"st","alpha3":"stp","name":"Sao Tome and Principe"}, 151 | {"id":682,"alpha2":"sa","alpha3":"sau","name":"Saudi Arabia"}, 152 | {"id":686,"alpha2":"sn","alpha3":"sen","name":"Senegal"}, 153 | {"id":688,"alpha2":"rs","alpha3":"srb","name":"Serbia"}, 154 | {"id":690,"alpha2":"sc","alpha3":"syc","name":"Seychelles"}, 155 | {"id":694,"alpha2":"sl","alpha3":"sle","name":"Sierra Leone"}, 156 | {"id":702,"alpha2":"sg","alpha3":"sgp","name":"Singapore"}, 157 | {"id":703,"alpha2":"sk","alpha3":"svk","name":"Slovakia"}, 158 | {"id":705,"alpha2":"si","alpha3":"svn","name":"Slovenia"}, 159 | {"id":90,"alpha2":"sb","alpha3":"slb","name":"Solomon Islands"}, 160 | {"id":706,"alpha2":"so","alpha3":"som","name":"Somalia"}, 161 | {"id":710,"alpha2":"za","alpha3":"zaf","name":"South Africa"}, 162 | {"id":728,"alpha2":"ss","alpha3":"ssd","name":"South Sudan"}, 163 | {"id":724,"alpha2":"es","alpha3":"esp","name":"Spain"}, 164 | {"id":144,"alpha2":"lk","alpha3":"lka","name":"Sri Lanka"}, 165 | {"id":729,"alpha2":"sd","alpha3":"sdn","name":"Sudan"}, 166 | {"id":740,"alpha2":"sr","alpha3":"sur","name":"Suriname"}, 167 | {"id":752,"alpha2":"se","alpha3":"swe","name":"Sweden"}, 168 | {"id":756,"alpha2":"ch","alpha3":"che","name":"Switzerland"}, 169 | {"id":760,"alpha2":"sy","alpha3":"syr","name":"Syrian Arab Republic"}, 170 | {"id":762,"alpha2":"tj","alpha3":"tjk","name":"Tajikistan"}, 171 | {"id":834,"alpha2":"tz","alpha3":"tza","name":"Tanzania, United Republic of"}, 172 | {"id":764,"alpha2":"th","alpha3":"tha","name":"Thailand"}, 173 | {"id":626,"alpha2":"tl","alpha3":"tls","name":"Timor-Leste"}, 174 | {"id":768,"alpha2":"tg","alpha3":"tgo","name":"Togo"}, 175 | {"id":776,"alpha2":"to","alpha3":"ton","name":"Tonga"}, 176 | {"id":780,"alpha2":"tt","alpha3":"tto","name":"Trinidad and Tobago"}, 177 | {"id":788,"alpha2":"tn","alpha3":"tun","name":"Tunisia"}, 178 | {"id":792,"alpha2":"tr","alpha3":"tur","name":"Türkiye"}, 179 | {"id":795,"alpha2":"tm","alpha3":"tkm","name":"Turkmenistan"}, 180 | {"id":798,"alpha2":"tv","alpha3":"tuv","name":"Tuvalu"}, 181 | {"id":800,"alpha2":"ug","alpha3":"uga","name":"Uganda"}, 182 | {"id":804,"alpha2":"ua","alpha3":"ukr","name":"Ukraine"}, 183 | {"id":784,"alpha2":"ae","alpha3":"are","name":"United Arab Emirates"}, 184 | {"id":826,"alpha2":"gb","alpha3":"gbr","name":"United Kingdom of Great Britain and Northern Ireland"}, 185 | {"id":840,"alpha2":"us","alpha3":"usa","name":"United States of America"}, 186 | {"id":858,"alpha2":"uy","alpha3":"ury","name":"Uruguay"}, 187 | {"id":860,"alpha2":"uz","alpha3":"uzb","name":"Uzbekistan"}, 188 | {"id":548,"alpha2":"vu","alpha3":"vut","name":"Vanuatu"}, 189 | {"id":862,"alpha2":"ve","alpha3":"ven","name":"Venezuela (Bolivarian Republic of)"}, 190 | {"id":704,"alpha2":"vn","alpha3":"vnm","name":"Viet Nam"}, 191 | {"id":887,"alpha2":"ye","alpha3":"yem","name":"Yemen"}, 192 | {"id":894,"alpha2":"zm","alpha3":"zmb","name":"Zambia"}, 193 | {"id":716,"alpha2":"zw","alpha3":"zwe","name":"Zimbabwe"}] -------------------------------------------------------------------------------- /src/deepCompareAndMerge.ts: -------------------------------------------------------------------------------- 1 | import { translateTextOrObject } from './translateTextAndObjects' 2 | 3 | interface CollectionObjType { 4 | [prop: string]: any // You can replace 'any' with a more specific type 5 | } 6 | 7 | let colllectionObj: CollectionObjType = {} // Assuming targetObj is initialized somewhere 8 | 9 | interface ReturnTypeExample {} 10 | 11 | export async function deepCompareTranslateAndMerge( 12 | newOriginalObj: CollectionObjType, 13 | originalObj: CollectionObjType, 14 | targetObj: CollectionObjType, 15 | fields: string[], 16 | language: string, 17 | action?: 'create' | 'update', 18 | onlyMissing?: boolean, 19 | sourceLanguage?: string, 20 | settings?: any, 21 | ): Promise { 22 | if (Array.isArray(newOriginalObj)) { 23 | return Promise.all( 24 | newOriginalObj.map((item, index) => 25 | deepCompareTranslateAndMerge( 26 | item, 27 | originalObj?.[index], 28 | targetObj?.[index], 29 | fields, 30 | language, 31 | action, 32 | onlyMissing, 33 | sourceLanguage, 34 | settings, 35 | ), 36 | ), 37 | ) 38 | } else if (typeof newOriginalObj === 'object' && newOriginalObj !== null) { 39 | const promises = Object.keys(newOriginalObj).map(async prop => { 40 | if (newOriginalObj.hasOwnProperty(prop)) { 41 | if (fields.includes(prop) /*&& typeof newOriginalObj[prop] === 'string'*/) { 42 | if ( 43 | originalObj?.[prop] === undefined || 44 | JSON.stringify(newOriginalObj[prop]) !== JSON.stringify(originalObj[prop]) || 45 | action === 'create' 46 | ) { 47 | console.log( 48 | 'onlyMissing', 49 | newOriginalObj[prop], 50 | onlyMissing, 51 | !onlyMissing || targetObj[prop] === undefined || targetObj[prop] === '', 52 | ) 53 | // Translate the text and merge it into the target language object 54 | if (!onlyMissing || targetObj[prop] === undefined || targetObj[prop] === '') { 55 | targetObj[prop] = await translateTextOrObject({ 56 | text: newOriginalObj[prop], 57 | language, 58 | sourceLanguage, 59 | settings, 60 | }) 61 | } else { 62 | // targetObj[prop] = 'not translated' 63 | } 64 | } 65 | } else if ( 66 | typeof newOriginalObj[prop] === 'object' && 67 | typeof targetObj[prop] === 'object' 68 | ) { 69 | targetObj[prop] = await deepCompareTranslateAndMerge( 70 | newOriginalObj[prop], 71 | originalObj?.[prop] || null, 72 | targetObj[prop] || null, 73 | fields, 74 | language, 75 | action, 76 | onlyMissing, 77 | sourceLanguage, 78 | settings, 79 | ) 80 | } 81 | } 82 | }) 83 | await Promise.all(promises) 84 | } 85 | return targetObj 86 | } 87 | -------------------------------------------------------------------------------- /src/fsMock.js: -------------------------------------------------------------------------------- 1 | module.exports = { fs: { createReadStream: () => null } } 2 | -------------------------------------------------------------------------------- /src/generateImage.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | 3 | export async function translateTextOrObject( 4 | text: string | object, 5 | language: string, 6 | sourceLanguage?: string, 7 | ) { 8 | if (typeof text !== 'string') { 9 | return text 10 | } 11 | const openai = new OpenAI({ 12 | apiKey: process.env.OPENAI_API_KEY, 13 | }) 14 | 15 | const promptAbstractBlogPost = (text: string) => { 16 | return ` 17 | Generate an abstract illustration for this blog post: 18 | 19 | ${text} 20 | ` 21 | } 22 | 23 | const content = 24 | typeof text !== 'string' 25 | ? promptAbstractBlogPost(JSON.stringify(text, null, 2)) 26 | : promptAbstractBlogPost(text) 27 | 28 | const response = await openai.images.generate({ 29 | model: 'dall-e-3', 30 | prompt: content, 31 | n: 1, 32 | size: '1024x1024', 33 | }) 34 | //image_url = response.data.data[0].url 35 | 36 | console.log('Translated', language, content) 37 | 38 | return 'url' //image_url 39 | } 40 | -------------------------------------------------------------------------------- /src/generateText.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | import { PluginTypes } from './types' 3 | import { PayloadHandler } from 'payload/config' 4 | import { validateAccess } from './access/validateAccess' 5 | 6 | export async function generateText(body: OpenAI.Chat.ChatCompletionCreateParams) { 7 | const openai = new OpenAI({ 8 | apiKey: process.env.OPENAI_API_KEY, 9 | }) 10 | 11 | const chatCompletion: any = await openai.chat.completions.create({ 12 | // model: 'gpt-3.5-turbo', 13 | ...body, 14 | }) 15 | 16 | const newItemResult = chatCompletion.choices[0].message.content 17 | 18 | return chatCompletion 19 | } 20 | 21 | export const generateTextHandler = (pluginOptions: PluginTypes): PayloadHandler => { 22 | return async (req, res) => { 23 | if (!validateAccess(req, res, pluginOptions)) return 24 | const result = await generateText(req.body) 25 | res.json(result) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/handleMissingTranslate.ts: -------------------------------------------------------------------------------- 1 | import { validateAccess } from './access/validateAccess' 2 | import { translateCollection } from './aiTranslate' 3 | import { PluginTypes } from './types' 4 | import { PayloadHandler } from 'payload/config' 5 | 6 | export const createMissingTranslatorHandler = (pluginOptions: PluginTypes): PayloadHandler => { 7 | return async (req, res) => { 8 | if (!validateAccess(req, res, pluginOptions)) return 9 | 10 | const allDocs = await req.payload.find({ 11 | collection: req.collection.config.slug, 12 | locale: req.body.locale, 13 | limit: 10000, 14 | }) 15 | 16 | if (!allDocs?.docs) return res.status(404).send() 17 | console.log('translate all docs', allDocs?.docs.length) 18 | for (const singleDoc of allDocs.docs) { 19 | const doc = await req.payload.findByID({ 20 | collection: req.collection.config.slug, 21 | id: singleDoc.id, 22 | locale: singleDoc.sourceLanguage || req.body.locale, 23 | }) 24 | 25 | console.log('doc', doc.sourceLanguage, req.body.codes) 26 | 27 | const collectionOptions = pluginOptions.collections[req.collection.config.slug] 28 | 29 | const settings = { 30 | ...(req.body.settings || {}), 31 | ...collectionOptions.settings, 32 | } 33 | 34 | const result = await translateCollection({ 35 | doc, 36 | req, 37 | previousDoc: {}, 38 | context: {}, 39 | collectionOptions, 40 | collection: req.collection.config, 41 | onlyMissing: req.body.onlyMissing, 42 | codes: req.body.codes, 43 | sourceLanguage: doc.sourceLanguage || req.body.locale, 44 | settings: { ...settings }, 45 | }) 46 | } 47 | 48 | const translated = { result: 'translated' } 49 | console.log('dooone') 50 | res.json(translated) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/handleTranslate.ts: -------------------------------------------------------------------------------- 1 | import { validateAccess } from './access/validateAccess' 2 | import { translateCollection } from './aiTranslate' 3 | import { PluginTypes } from './types' 4 | import { PayloadHandler } from 'payload/config' 5 | 6 | export const createTranslatorHandler = (pluginOptions: PluginTypes): PayloadHandler => { 7 | return async (req, res) => { 8 | const doc = await req.payload.findByID({ 9 | collection: req.collection.config.slug, 10 | id: req.body.id, 11 | locale: req.body.locale, 12 | }) 13 | 14 | if (!doc) return res.status(404).send() 15 | 16 | const collectionOptions = pluginOptions.collections[req.collection.config.slug] 17 | 18 | if (!validateAccess(req, res, pluginOptions)) return 19 | 20 | const settings = { 21 | ...(req.body.settings || {}), 22 | ...collectionOptions.settings, 23 | } 24 | 25 | const result = await translateCollection({ 26 | doc, 27 | req, 28 | previousDoc: {}, 29 | context: {}, 30 | collectionOptions, 31 | collection: req.collection.config, 32 | onlyMissing: req.body.onlyMissing, 33 | codes: req.body.codes, 34 | sourceLanguage: req.body.locale, 35 | settings: { ...settings }, 36 | }) 37 | /*if (translatorConfig.access) { 38 | const hasAccesses = await translatorConfig.access(req) 39 | if (!hasAccesses) res.status(403).send() 40 | } else { 41 | if (!req.user) return res.status(403).send() 42 | } 43 | */ 44 | const translated = { result: 'translated' } 45 | res.json(translated) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { aiTranslatorPlugin } from './plugin' 2 | export type { PluginTypes } from './types' 3 | 4 | export { generateTitle, generateDescription } from './seoTools' 5 | -------------------------------------------------------------------------------- /src/mocks/mockFile.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /src/onInitExtension.ts: -------------------------------------------------------------------------------- 1 | import type { Payload } from 'payload/dist/payload' 2 | 3 | import type { PluginTypes } from './types' 4 | 5 | export const onInitExtension = (pluginOptions: PluginTypes, payload: Payload): void => { 6 | const { express: app } = payload 7 | 8 | if (!app) return 9 | 10 | try { 11 | // You can use the existing express app here to add middleware, routes, etc. 12 | // app.use(...) 13 | } catch (err: unknown) { 14 | // payload.logger(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Config, Plugin } from 'payload/config' 2 | 3 | import { onInitExtension } from './onInitExtension' 4 | import type { PluginTypes } from './types' 5 | import { extendWebpackConfig } from './webpack' 6 | import AfterDashboard from './components/AfterDashboard' 7 | import stringTranslations from './stringTranslations' 8 | import { createTranslatorHandler } from './handleTranslate' 9 | import { Translator } from './components/Translator' 10 | import { Field } from 'payload/types' 11 | import { GenerateMetadata } from './components/Metadata' 12 | import { generateText, generateTextHandler } from './generateText' 13 | import aiCaptionHook from './aiCaption' 14 | 15 | type PluginType = (pluginOptions: PluginTypes) => Plugin 16 | 17 | export const aiTranslatorPlugin = 18 | (pluginOptions: PluginTypes): Plugin => 19 | incomingConfig => { 20 | const { collections: allCollectionOptions, enabled } = pluginOptions 21 | let config = { ...incomingConfig } 22 | 23 | // If you need to add a webpack alias, use this function to extend the webpack config 24 | const webpack = extendWebpackConfig(incomingConfig) 25 | 26 | console.log('pluginOptions', config.collections) 27 | 28 | config.collections = (config.collections || []).map(existingCollection => { 29 | console.log('existingCollection', existingCollection) 30 | 31 | const collectionOptions = {} 32 | if (existingCollection.slug !== 'media') return existingCollection 33 | 34 | return { 35 | ...existingCollection, 36 | 37 | endpoints: [ 38 | ...(existingCollection.endpoints || []), 39 | /* { 40 | path: '/translate', 41 | method: 'post', 42 | handler: createTranslatorHandler(pluginOptions), 43 | }, */ 44 | ], 45 | hooks: { 46 | ...(existingCollection.hooks || {}), 47 | afterChange: [ 48 | ...(existingCollection.hooks?.afterChange || []), 49 | aiCaptionHook({ collectionOptions, pluginOptions, collection: existingCollection }), 50 | ], 51 | }, 52 | } 53 | }) 54 | 55 | config.collections = (config.collections || []).map(existingCollection => { 56 | const collectionOptions = allCollectionOptions[existingCollection.slug] 57 | 58 | /*if (options?.adapter) { 59 | const adapter = options.adapter({ 60 | collection: existingCollection, 61 | prefix: options.prefix, 62 | }) 63 | 64 | if (adapter.onInit) initFunctions.push(adapter.onInit) 65 | 66 | const fields = getFields({ 67 | collection: existingCollection, 68 | disablePayloadAccessControl: options.disablePayloadAccessControl, 69 | generateFileURL: options.generateFileURL, 70 | prefix: options.prefix, 71 | adapter, 72 | }) 73 | 74 | const handlers = [ 75 | ...(typeof existingCollection.upload === 'object' && 76 | Array.isArray(existingCollection.upload.handlers) 77 | ? existingCollection.upload.handlers 78 | : []), 79 | ] 80 | 81 | if (!options.disablePayloadAccessControl) { 82 | handlers.push(adapter.staticHandler) 83 | }*/ 84 | 85 | if (!collectionOptions) return existingCollection 86 | 87 | return { 88 | ...existingCollection, 89 | 90 | endpoints: [ 91 | ...(existingCollection.endpoints || []), 92 | { 93 | path: '/translate', 94 | method: 'post', 95 | handler: createTranslatorHandler(pluginOptions), 96 | }, 97 | ], 98 | hooks: { 99 | ...(existingCollection.hooks || {}), 100 | afterChange: [ 101 | ...(existingCollection.hooks?.afterChange || []), 102 | // aiTranslate({ collectionOptions, collection: existingCollection }), 103 | // getBeforeChangeHook({ adapter, collection: existingCollection }), 104 | ], 105 | }, 106 | fields: [ 107 | ...(existingCollection.fields || []), 108 | { 109 | name: 'translator', 110 | type: 'ui', 111 | admin: { 112 | position: 'sidebar', 113 | components: { 114 | Field: Translator, 115 | }, 116 | }, 117 | } as Field, 118 | /*{ 119 | name: 'metadateGenerator', 120 | type: 'ui', 121 | admin: { 122 | position: 'sidebar', 123 | components: { 124 | Field: GenerateMetadata, 125 | }, 126 | }, 127 | } as Field,*/ 128 | ], 129 | } 130 | //} 131 | 132 | return existingCollection 133 | }) 134 | 135 | config.admin = { 136 | ...(config.admin || {}), 137 | // If you extended the webpack config, add it back in here 138 | // If you did not extend the webpack config, you can remove this line 139 | webpack, 140 | 141 | // Add additional admin config here 142 | 143 | components: { 144 | ...(config.admin?.components || {}), 145 | // Add additional admin components here 146 | afterDashboard: [...(config.admin?.components?.afterDashboard || []), AfterDashboard], 147 | }, 148 | } 149 | 150 | // If the plugin is disabled, return the config without modifying it 151 | // The order of this check is important, we still want any webpack extensions to be applied even if the plugin is disabled 152 | if (pluginOptions.enabled === false) { 153 | return config 154 | } 155 | 156 | config.collections = [...(config.collections || []), stringTranslations(pluginOptions)] 157 | 158 | config.globals = [ 159 | ...(config.globals || []), 160 | // Add additional globals here 161 | ] 162 | 163 | config.hooks = { 164 | ...(config.hooks || {}), 165 | // Add additional hooks here 166 | } 167 | 168 | config.endpoints = [ 169 | ...(config.endpoints || []), 170 | { 171 | path: '/generate-text', 172 | method: 'post', 173 | handler: generateTextHandler(pluginOptions), 174 | }, 175 | ] 176 | 177 | config.onInit = async payload => { 178 | if (incomingConfig.onInit) await incomingConfig.onInit(payload) 179 | // Add additional onInit code by using the onInitExtension function 180 | onInitExtension(pluginOptions, payload) 181 | } 182 | 183 | return config 184 | } 185 | -------------------------------------------------------------------------------- /src/seoTools.ts: -------------------------------------------------------------------------------- 1 | export const generateTitle = async ({ doc, locale }: any) => { 2 | console.log('sourceLanguage', locale) 3 | const body = { 4 | model: 'gpt-4o', 5 | messages: [ 6 | { 7 | role: 'system', 8 | content: ` 9 | You will be provided with a blog post as json. 10 | Craft a title of the blog post for Google SEO purpose, which should be between 50 and 60 characters. 11 | Return the title in the language with ISO-2-Code: "${locale.code}". 12 | Only return the title without any comment or quotes, not the rest of the blog post. 13 | `, 14 | }, 15 | { 16 | role: 'user', 17 | content: `${JSON.stringify(doc, null, 2).substring(0, 2000)}`, 18 | }, 19 | ], 20 | max_tokens: 30, 21 | } 22 | 23 | try { 24 | const response = await fetch(`/api/generate-text`, { 25 | method: 'POST', 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | }, 29 | body: JSON.stringify(body) as any, 30 | }) 31 | 32 | const chatCompletion = await response.json() 33 | return chatCompletion.choices[0].message.content 34 | } catch (error) { 35 | console.error(error) 36 | } 37 | } 38 | 39 | export const generateDescription = async ({ doc, locale }: any) => { 40 | const body = { 41 | model: 'gpt-4o', 42 | messages: [ 43 | { 44 | role: 'system', 45 | content: ` 46 | You will be provided with a blog post as json. Only use the values in the JSON object. 47 | Craft such a meta description for Google SEO for this blog post and output only the result (maximum 150 characters) without any comment or quotes. 48 | Return the title in the language with ISO-2-Code: "${locale.code}". 49 | Make sure it is shorter than 150 characters. 50 | Only return the title without any comment or quotes, not the rest of the blog post. 51 | `, 52 | }, 53 | { 54 | role: 'user', 55 | content: `${JSON.stringify(doc, null, 2).substring(0, 2000)}`, 56 | }, 57 | ], 58 | max_tokens: 50, 59 | } 60 | 61 | try { 62 | const response = await fetch(`/api/generate-text`, { 63 | method: 'POST', 64 | headers: { 65 | 'Content-Type': 'application/json', 66 | }, 67 | body: JSON.stringify(body) as any, 68 | }) 69 | 70 | const chatCompletion = await response.json() 71 | return chatCompletion.choices[0].message.content 72 | } catch (error) { 73 | console.error(error) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/stringTranslations.ts: -------------------------------------------------------------------------------- 1 | import { Block, CollectionConfig, Field } from 'payload/types' 2 | 3 | import { admins } from './access/admins' 4 | import { adminsOrPublished } from './access/adminsOrPublished' 5 | import { anyone } from './access/anyone' 6 | import aiTranslate from './aiTranslate' 7 | import { Translator } from './components/Translator' 8 | import { createTranslatorHandler } from './handleTranslate' 9 | import { validateAccess } from './access/validateAccess' 10 | import { createMissingTranslatorHandler } from './handleMissingTranslate' 11 | 12 | const TextAreaBlock: Block = { 13 | slug: 'translation-textarea', 14 | 15 | fields: [ 16 | { 17 | name: 'content', 18 | type: 'textarea', 19 | localized: true, 20 | }, 21 | { 22 | name: 'noAutoTranslate', 23 | type: 'checkbox', 24 | label: 'Do not auto-translate', 25 | localized: true, 26 | }, 27 | ], 28 | } 29 | 30 | const stringTranslations = (pluginOptions: any): CollectionConfig => { 31 | return { 32 | slug: 'translations', 33 | admin: { 34 | useAsTitle: 'key', 35 | }, 36 | access: { 37 | read: anyone, 38 | /*update: admins, 39 | create: admins, 40 | delete: admins,*/ 41 | }, 42 | 43 | hooks: { 44 | afterChange: [ 45 | aiTranslate({ 46 | collection: { slug: 'translations' }, 47 | collectionOptions: { fields: ['content'] }, 48 | pluginOptions, 49 | }), 50 | async (req: any) => { 51 | /*if (req.user && req.user.role === 'admin') { 52 | return req 53 | } 54 | 55 | /* 56 | return { 57 | ...req, 58 | where: { 59 | ...req.where, 60 | namespace: { 61 | equals: 'public', 62 | }, 63 | }, 64 | }*/ 65 | }, 66 | ], 67 | }, 68 | endpoints: [ 69 | { 70 | path: '/create-missing', 71 | //path: '/:id/tracking', 72 | method: 'post', 73 | handler: async (req: any, res: any, next: any) => { 74 | if (!validateAccess(req, res, pluginOptions)) return 75 | const posts = await req.payload.find({ 76 | collection: 'translations', 77 | where: { 78 | key: { 79 | equals: req.body.key, 80 | }, 81 | }, 82 | }) 83 | 84 | if (posts.docs.length > 0) { 85 | res.status(200).send(posts.docs) 86 | } else { 87 | const newPost = await req.payload.create({ 88 | collection: 'translations', 89 | locale: req.body.language, 90 | data: { 91 | key: req.body.key, 92 | namespace: req.body.namespace, 93 | sourceLanguage: req.body.language, 94 | translation: [ 95 | { 96 | content: req.body.content, 97 | blockType: 'translation-textarea', 98 | }, 99 | ], 100 | }, 101 | }) 102 | 103 | res.status(200).send(newPost) 104 | } 105 | }, 106 | }, 107 | { 108 | path: '/translate', 109 | method: 'post', 110 | handler: createTranslatorHandler({ 111 | ...pluginOptions, 112 | collections: { 113 | ...pluginOptions.collections, 114 | translations: { 115 | ...pluginOptions.collections?.translations, 116 | fields: ['content'], 117 | }, 118 | }, 119 | }), 120 | }, 121 | { 122 | path: '/translate-missing', 123 | method: 'post', 124 | handler: createMissingTranslatorHandler({ 125 | ...pluginOptions, 126 | collections: { 127 | ...pluginOptions.collections, 128 | translations: { 129 | ...pluginOptions.collections?.translations, 130 | fields: ['content'], 131 | }, 132 | }, 133 | }), 134 | }, 135 | ], 136 | fields: [ 137 | { 138 | type: 'row', 139 | fields: [ 140 | { 141 | name: 'key', 142 | type: 'text', 143 | required: true, 144 | admin: { 145 | width: '50%', 146 | }, 147 | }, 148 | { 149 | name: 'namespace', 150 | type: 'text', 151 | }, 152 | { 153 | name: 'sourceLanguage', 154 | type: 'text', 155 | admin: { 156 | width: '10%', 157 | }, 158 | }, 159 | ], 160 | }, 161 | { 162 | name: 'translation', 163 | type: 'blocks', 164 | minRows: 1, 165 | maxRows: 1, 166 | blocks: [TextAreaBlock], 167 | }, 168 | { 169 | name: 'translator', 170 | type: 'ui', 171 | admin: { 172 | position: 'sidebar', 173 | components: { 174 | Field: Translator, 175 | }, 176 | }, 177 | } as Field, 178 | ], 179 | } 180 | } 181 | 182 | export default stringTranslations 183 | -------------------------------------------------------------------------------- /src/translateTextAndObjects.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | import ISO6391 from 'iso-639-1' 3 | 4 | function isoToFullName(isoCode: string, settings: any) { 5 | const foundLanguage = settings.localization.locales.find((item: any) => item.code === isoCode) 6 | if (foundLanguage && foundLanguage.label.length > 2) { 7 | return foundLanguage.label 8 | } 9 | if (ISO6391.getName(isoCode)) return ISO6391.getName(isoCode) 10 | 11 | console.log('language not found') 12 | return isoCode 13 | } 14 | function messagesMarkdown({ sourceLanguage, text, language, settings }: any) { 15 | return [ 16 | { 17 | role: 'system', 18 | content: `You will be provided with markdown in "${isoToFullName( 19 | sourceLanguage, 20 | settings, 21 | )}", and your task is to translate it into the language: "${isoToFullName( 22 | language, 23 | settings, 24 | )}". Only return the translated markdown (mdx) and keep the structure.`, 25 | }, 26 | { 27 | role: 'user', 28 | content: `${text}`, 29 | }, 30 | ] 31 | } 32 | 33 | function messagesString({ sourceLanguage, text, language, settings }: any) { 34 | return [ 35 | { 36 | role: 'system', 37 | content: `You will be provided with text in "${isoToFullName( 38 | sourceLanguage, 39 | settings, 40 | )}", and your task is to translate it into the language:"${isoToFullName( 41 | language, 42 | settings, 43 | )}". Only return the translated text without anything else.`, 44 | }, 45 | { 46 | role: 'user', 47 | content: `${text}`, 48 | }, 49 | ] 50 | } 51 | 52 | function messagesWithJson({ sourceLanguage, text, language, settings }: any) { 53 | return [ 54 | { 55 | role: 'system', 56 | content: `You will be provided with lexical json structure in "${isoToFullName( 57 | sourceLanguage, 58 | settings, 59 | )}", and your task is to translate it into the language "${isoToFullName( 60 | language, 61 | settings, 62 | )}". Keep the json structure. Make sure NOT to wrap your result in markdown.`, 63 | }, 64 | { 65 | role: 'user', 66 | content: `${JSON.stringify(text /* , null, 2*/)}`, 67 | }, 68 | ] 69 | } 70 | 71 | function messagesWithJsonLexical({ sourceLanguage, text, language, settings }: any) { 72 | return [ 73 | { 74 | role: 'system', 75 | content: `You will be provided with a flat object structure with long keys in the language "${isoToFullName( 76 | sourceLanguage, 77 | settings, 78 | )}", and your task is to translate it into the language "${isoToFullName( 79 | language, 80 | settings, 81 | )}". Keep the flat json object structure with long dot seperated keys. Make sure NOT to wrap your result in markdown.`, 82 | }, 83 | { 84 | role: 'user', 85 | content: `${JSON.stringify(text /* , null, 2*/)}`, 86 | }, 87 | ] 88 | } 89 | 90 | function promptDefault({ messages }: any): any { 91 | return messages 92 | } 93 | 94 | function generateUniqueKey(path: any) { 95 | return path.join('.') 96 | } 97 | 98 | function extractAndCapitalizeText( 99 | node: any, 100 | path: any, 101 | textMap: any, 102 | isTargetProperty: (node: any, key: string) => boolean, 103 | ) { 104 | if (node !== null && typeof node === 'object') { 105 | Object.keys(node).forEach(key => { 106 | if (isTargetProperty(node, key)) { 107 | const keyPath = generateUniqueKey(path.concat([key])) 108 | textMap[keyPath] = node[key] // Assuming you want to capitalize the text. 109 | } else { 110 | extractAndCapitalizeText(node[key], path.concat([key]), textMap, isTargetProperty) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | function reapplyText( 117 | node: any, 118 | path: any, 119 | textMap: any, 120 | isTargetProperty: (node: any, key: string) => boolean, 121 | ) { 122 | if (node !== null && typeof node === 'object') { 123 | Object.keys(node).forEach(key => { 124 | if (isTargetProperty(node, key)) { 125 | const keyPath = generateUniqueKey(path.concat([key])) 126 | if (textMap[keyPath]) { 127 | node[key] = textMap[keyPath] 128 | } 129 | } else { 130 | reapplyText(node[key], path.concat([key]), textMap, isTargetProperty) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | interface TranslateTextOrObject { 137 | text: any 138 | language: string 139 | sourceLanguage?: string 140 | retryCount?: number 141 | setting: any 142 | } 143 | 144 | export async function translateTextOrObject({ 145 | text, 146 | language, 147 | sourceLanguage, 148 | retryCount = 0, 149 | settings, 150 | }: any) { 151 | console.log('settings', settings) 152 | function isTranslateNode(node: any, key: string) { 153 | return (key === 'text' && typeof node[key] === 'string') || key === 'name' 154 | } 155 | 156 | const textAsString = typeof text === 'string' ? text : JSON.stringify(text, null, 2) 157 | 158 | if (textAsString.length < 2) { 159 | return text 160 | } 161 | 162 | const openai = new OpenAI({ 163 | apiKey: process.env.OPENAI_API_KEY, 164 | }) 165 | try { 166 | let textMap = {} 167 | 168 | if (text?.root?.children) { 169 | extractAndCapitalizeText(text.root, ['root'], textMap, isTranslateNode) 170 | } 171 | 172 | const { promptFunc = promptDefault, namespace, localization, ...restSettings }: any = settings 173 | const languageIso = language === 'se' ? 'sv' : language 174 | 175 | const promptMessage: any = 176 | typeof text === 'string' && text.length < 400 177 | ? (messagesString({ sourceLanguage, text, language: languageIso, settings }) as any) 178 | : typeof text === 'string' 179 | ? (messagesMarkdown({ sourceLanguage, text, language: languageIso, settings }) as any) 180 | : text?.root?.children 181 | ? (messagesWithJsonLexical({ 182 | sourceLanguage, 183 | text: textMap, 184 | language: languageIso, 185 | settings, 186 | }) as any) 187 | : (messagesWithJson({ sourceLanguage, text, language: languageIso, settings }) as any) 188 | 189 | const finalPrompt = promptFunc({ 190 | messages: promptMessage, 191 | namespace, 192 | sourceLanguage, 193 | language, 194 | settings, 195 | }) 196 | 197 | const chatCompletion = await openai.chat.completions.create({ 198 | model: /* textAsString.length > 2000 ? 'gpt-3.5-turbo-16k' :*/ 'gpt-4o', /// 'gpt-3.5-turbo', // gpt-3.5-turbo-1106 // gpt-3.5-turbo-16k-0613 199 | messages: finalPrompt, 200 | temperature: 0, 201 | max_tokens: 4096, 202 | top_p: 1, 203 | frequency_penalty: 0, 204 | presence_penalty: 0, 205 | ...restSettings, 206 | }) 207 | 208 | if (text?.root?.children) { 209 | const newText = JSON.parse(JSON.stringify(text)) 210 | console.log( 211 | 'chatCompletion.choices[0].message.content', 212 | chatCompletion.choices[0].message.content, 213 | ) 214 | reapplyText( 215 | newText.root, 216 | ['root'], 217 | JSON.parse(chatCompletion.choices[0].message.content as string), 218 | isTranslateNode, 219 | ) 220 | return newText 221 | } 222 | 223 | const newItemResult = 224 | typeof text !== 'string' 225 | ? JSON.parse(chatCompletion.choices[0].message.content as string) 226 | : chatCompletion.choices[0].message.content 227 | 228 | return newItemResult 229 | } catch (error: any) { 230 | if (error.status === 429) { 231 | console.log( 232 | `Too many requests. Retry after ${error.headers['retry-after-ms']}ms. Retry count: ${retryCount}`, 233 | ) 234 | 235 | await new Promise(resolve => setTimeout(resolve, error.headers['retry-after-ms'])) 236 | const newResult: any = await translateTextOrObject({ 237 | text, 238 | language, 239 | sourceLanguage, 240 | retryCount: retryCount + 1, 241 | settings, 242 | }) 243 | 244 | return newResult 245 | } else { 246 | console.log( 247 | 'Could not be translated', 248 | error /* , chatCompletion.choices[0].message.content */, 249 | ) 250 | } 251 | } 252 | } 253 | 254 | /*export async function translateLongTextOrObject( 255 | text: string | object, 256 | language: string, 257 | sourceLanguage?: string, 258 | retryCount: number = 0, 259 | ) { 260 | const textAsString = typeof text === 'string' ? text : JSON.stringify(text, null, 2) 261 | 262 | if (textAsString.length < 400) { 263 | return translateTextOrObject(text, language, sourceLanguage) 264 | } 265 | 266 | if (typeof text !== 'string' && text.root.children) { 267 | console.log('text', text.root.children) 268 | const textAsArray = Object.keys(text.root.children).map(key => text[key]) 269 | 270 | const textAsArrayTranslated = await Promise.all( 271 | textAsArray.map(async (item, index) => { 272 | const newItemResult = await translateLongTextOrObject( 273 | item, 274 | language, 275 | sourceLanguage, 276 | retryCount, 277 | ) 278 | return newItemResult 279 | }), 280 | ) 281 | 282 | const textAsObject = Object.keys(text.root.children).reduce((acc, key, index) => { 283 | acc[key] = textAsArrayTranslated[index] 284 | return acc 285 | }, {}) 286 | 287 | text.root.children = textAsObject 288 | return text 289 | } 290 | }*/ 291 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface PluginTypes { 2 | /** 3 | * Enable or disable plugin 4 | * @default false 5 | */ 6 | enabled?: boolean 7 | /** 8 | * Collection options 9 | */ 10 | collections: { 11 | [key: string]: any 12 | } 13 | } 14 | 15 | export interface NewCollectionTypes { 16 | title: string 17 | } 18 | 19 | export interface TranslatorConfig { 20 | name: string 21 | type: string 22 | } 23 | -------------------------------------------------------------------------------- /src/webpack.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import type { Config } from 'payload/config' 3 | import type { Configuration as WebpackConfig } from 'webpack' 4 | 5 | export const extendWebpackConfig = 6 | (config: Config): ((webpackConfig: WebpackConfig) => WebpackConfig) => 7 | webpackConfig => { 8 | const existingWebpackConfig = 9 | typeof config.admin?.webpack === 'function' 10 | ? config.admin.webpack(webpackConfig) 11 | : webpackConfig 12 | 13 | const mockModulePath = path.resolve(__dirname, './mocks/mockFile.js') 14 | 15 | const newWebpack = { 16 | ...existingWebpackConfig, 17 | resolve: { 18 | ...(existingWebpackConfig.resolve || {}), 19 | alias: { 20 | ...(existingWebpackConfig.resolve?.alias ? existingWebpackConfig.resolve.alias : {}), 21 | fs: path.resolve(__dirname, './fsMock.js'), 22 | // Add additional aliases here like so: 23 | [path.resolve(__dirname, './yourFileHere')]: mockModulePath, 24 | }, 25 | }, 26 | } 27 | 28 | return newWebpack 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "target": "es5", 9 | "outDir": "./dist", 10 | "allowJs": true, 11 | "module": "commonjs", 12 | "sourceMap": true, 13 | "jsx": "react", 14 | "esModuleInterop": true, 15 | "declaration": true, 16 | "declarationDir": "./dist", 17 | "skipLibCheck": true, 18 | "strict": true, 19 | }, 20 | "include": [ 21 | "src/**/*", 22 | ], 23 | } --------------------------------------------------------------------------------