├── dev ├── secrets │ └── api-key-in-file.txt ├── extensions │ └── directus-extension-auto-translation │ │ ├── .gitignore │ │ ├── src │ │ ├── MyTranslatorInterface.ts │ │ ├── helper │ │ │ ├── ItemsServiceCreator.ts │ │ │ └── CollectionsServiceCreator.ts │ │ ├── Translator.ts │ │ ├── TranslatorSettings.ts │ │ ├── DeepLTranslator.ts │ │ ├── index.ts │ │ ├── schema │ │ │ └── schema.ts │ │ └── DirectusCollectionTranslator.ts │ │ ├── tsconfig.json │ │ └── package.json ├── exportScheme.sh ├── docker-compose.yaml └── scheme.yaml ├── assets └── translate-animation.gif ├── sonar-project.properties ├── .github └── workflows │ └── npmPublish.yml ├── CI-CD-Configuration.md ├── .gitignore └── README.md /dev/secrets/api-key-in-file.txt: -------------------------------------------------------------------------------- 1 | a0059e4a-2fb6-a84a-a88e-b2f455281703 -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /assets/translate-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FireboltCasters/directus-extension-auto-translation/HEAD/assets/translate-animation.gif -------------------------------------------------------------------------------- /dev/exportScheme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Hello" 3 | sudo DB_CLIENT=sqlite3 DB_FILENAME=./database/data.db npx directus schema snapshot --yes ./scheme.yaml && echo 'Scheme 4 | exported to ./scheme.yaml' 5 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/src/MyTranslatorInterface.ts: -------------------------------------------------------------------------------- 1 | export interface MyTranslatorInterface { 2 | 3 | init(): Promise; 4 | 5 | translate(text: string, source_language: string, destination_language: string): Promise; 6 | 7 | getUsage(): Promise; 8 | 9 | getExtra(): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=FireboltCasters_directus-extension-auto-translation 2 | sonar.organization=fireboltcasters 3 | sonar.javascript.lcov.reportPaths=./coverage/lcov.info 4 | sonar.coverage.exclusions=src/tests/**/*,src/ignoreCoverage/**/*,babel.config.js 5 | sonar.exclusions=src/tests/**/*,src/ignoreCoverage/**/*,babel.config.js 6 | sonar.qualitygate.wait=true 7 | sonar.qualitygate.timeout=300 8 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/src/helper/ItemsServiceCreator.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/directus/directus/blob/main/api/src/services/items.ts 2 | export class ItemsServiceCreator { 3 | 4 | constructor(services, database, schema) { 5 | this.services = services; 6 | this.database = database; 7 | this.schema = schema; 8 | } 9 | 10 | getItemsService(tablename) { 11 | const {ItemsService} = this.services; 12 | return new ItemsService(tablename, { 13 | accountability: null, //this makes us admin 14 | knex: this.database, //TODO: i think this is not neccessary 15 | schema: this.schema, 16 | }); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/src/helper/CollectionsServiceCreator.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/directus/directus/blob/main/api/src/services/items.ts 2 | export class CollectionsServiceCreator { 3 | 4 | constructor(services, database, schema) { 5 | this.services = services; 6 | this.database = database; 7 | this.schema = schema; 8 | } 9 | 10 | getCollectionsService() { 11 | const {CollectionsService} = this.services; 12 | return new CollectionsService({ 13 | accountability: null, //this makes us admin 14 | knex: this.database, //TODO: i think this is not neccessary 15 | schema: this.schema, 16 | }); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "lib": ["ES2019", "DOM"], 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noUnusedParameters": true, 15 | "alwaysStrict": true, 16 | "strictNullChecks": true, 17 | "strictFunctionTypes": true, 18 | "strictBindCallApply": true, 19 | "strictPropertyInitialization": true, 20 | "resolveJsonModule": false, 21 | "skipLibCheck": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "allowSyntheticDefaultImports": true, 24 | "isolatedModules": true, 25 | "rootDir": "./src" 26 | }, 27 | "include": ["./src/**/*.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/npmPublish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | jobs: 8 | build: 9 | if: ${{ !startsWith(github.event.head_commit.message, '[RELEASE]') }} 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '18.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: git config --global user.name 'Firebolt Caster' 18 | - run: git config --global user.email 'nilsbaumgartner@live.de' 19 | - run: cp README.md ./dev/extensions/directus-extension-auto-translation/README.md 20 | - run: cd ./dev/extensions/directus-extension-auto-translation && npm ci 21 | - run: cd ./dev/extensions/directus-extension-auto-translation && npm run release 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /dev/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | directus: 5 | container_name: directus-auto-translation 6 | image: directus/directus:10.11.0 7 | env_file: 8 | - .env 9 | ports: 10 | - 8055:8055 11 | volumes: 12 | # By default, uploads are stored in /directus/uploads 13 | # Always make sure your volumes matches the storage root when using 14 | # local driver 15 | - ./uploads:/directus/uploads 16 | # Make sure to also mount the volume when using SQLite 17 | - ./database:/directus/database 18 | # If you want to load extensions from builded files 19 | #- ./auto-backup-hook/dist/index.js:/directus/extensions/hooks/auto-backup-hook/index.js 20 | # Hotreload dev 21 | - ./extensions:/directus/extensions/ 22 | networks: 23 | - directus 24 | environment: 25 | KEY: '255d861b-5ea1-5996-9aa3-922530ec40b1' 26 | SECRET: '6116487b-cda1-52c2-b5b5-c8022c45e263' 27 | 28 | EXTENSIONS_AUTO_RELOAD: 'true' 29 | 30 | DB_CLIENT: 'sqlite3' # https://docs.directus.io/configuration/config-options/ 31 | DB_FILENAME: './database/data.db' 32 | 33 | CACHE_ENABLED: 'false' 34 | 35 | ADMIN_EMAIL: 'admin@example.com' 36 | ADMIN_PASSWORD: 'd1r3ctu5' 37 | 38 | AUTO_TRANSLATE_API_KEY: "${AUTO_TRANSLATE_API_KEY}" 39 | #AUTO_TRANSLATE_API_KEY_SAVING_PATH: "/directus/secrets/api-key-in-file.txt" 40 | networks: 41 | directus: 42 | -------------------------------------------------------------------------------- /CI-CD-Configuration.md: -------------------------------------------------------------------------------- 1 | # CI/CD-Configuration 2 | 3 | ## Used Pipelines 4 | 5 | - SonarCloud 6 | - Lint Action 7 | 8 | ### README.md 9 | 10 | - Change all Badges to correct url 11 | 12 | ### SonarCloud 13 | 14 | `for new project` 15 | 16 | - Checks any Code-Smells and Code-Quality metrics 17 | - Activate account and connect with GitHub 18 | - https://sonarcloud.io/projects 19 | - Add the project 20 | - https://sonarcloud.io/projects/create 21 | - Add .github/workflows/build.yml to the repo 22 | - Check correct branch: master-->main 23 | - Add sonar-project.properties 24 | - Add `SONAR_TOKEN` to Git-Repo 25 | - Configure New Code after first analysis to "Previous version" 26 | - All code that has changed since the previous version bump is considered new code 27 | 28 | ### NPM Publish CI/CD 29 | 30 | `for new project` 31 | 32 | - Add new Secret to Git-Repo: NPM_TOKEN 33 | - https://docs.npmjs.com/creating-and-viewing-access-tokens 34 | - Add new Secret to Git-Repo: GH_PERSONAL_ACCESS_TOKEN 35 | - Select scopes: `repo` 36 | - https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token 37 | - from Tutorial: https://aboutbits.it/blog/2021-03-11-using-github-actions-to-perfom-npm-version-increment 38 | 39 | ### Lint Action 40 | 41 | - Automatically formats code to meet linting requirements 42 | - https://github.com/marketplace/actions/lint-action 43 | - https://github.com/wearerequired/lint-action 44 | - Setup in .github/workflows/build.yml 45 | - activate: auto_fix 46 | - add .github to .eslintignore and .prettierignore 47 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directus-extension-auto-translation", 3 | "description": "An extension for Directus that automatically translates fields using DeepL API when provided with an API key.", 4 | "icon": "extension", 5 | "version": "10.11.8", 6 | "keywords": [ 7 | "translate", 8 | "translation", 9 | "deepl", 10 | "ai", 11 | "auto", 12 | "automatic", 13 | "language", 14 | "multilingual", 15 | "localization", 16 | "internationalization", 17 | "i18n", 18 | "l10n", 19 | "directus", 20 | "directus-extension", 21 | "directus-extension-hook" 22 | ], 23 | "type": "module", 24 | "files": [ 25 | "dist" 26 | ], 27 | "directus:extension": { 28 | "type": "hook", 29 | "path": "dist/index.js", 30 | "source": "src/index.ts", 31 | "host": "^10.10.0" 32 | }, 33 | "scripts": { 34 | "build": "directus-extension build", 35 | "dev": "directus-extension build -w --no-minify", 36 | "link": "directus-extension link", 37 | "release": "release-it --npm.skipChecks", 38 | "clean": "rm -rf ./dist" 39 | }, 40 | "release-it": { 41 | "hooks": { 42 | "before:init": [ 43 | "npm run build" 44 | ] 45 | }, 46 | "git": { 47 | "commitMessage": "[RELEASE] ${version}", 48 | "tagName": "v${version}" 49 | }, 50 | "npm": { 51 | "publish": true 52 | }, 53 | "github": { 54 | "release": true 55 | } 56 | }, 57 | "dependencies": { 58 | "deepl-node": "^1.5.0", 59 | "js-yaml": "^4.1.0" 60 | }, 61 | "devDependencies": { 62 | "@directus/extensions-sdk": "11.0.4", 63 | "@types/js-yaml": "^4.0.9", 64 | "@types/node": "^20.12.12", 65 | "release-it": "^14.2.2", 66 | "rimraf": "^3.0.2", 67 | "typescript": "^5.4.5" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | dev/auto-translation-hook/README.md 10 | dev/auto-translation-hook/dist/index.js 11 | 12 | dev/database/** 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | build/ 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # next.js build output 79 | .next 80 | 81 | # nuxt.js build output 82 | .nuxt 83 | 84 | # vuepress build output 85 | .vuepress/dist 86 | 87 | # Serverless directories 88 | .serverless/ 89 | 90 | # FuseBox cache 91 | .fusebox/ 92 | 93 | # DynamoDB Local files 94 | .dynamodb/ 95 | 96 | .idea/ 97 | 98 | .DS_Store 99 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/src/Translator.ts: -------------------------------------------------------------------------------- 1 | import {DeepLTranslator} from './DeepLTranslator'; 2 | import {MyTranslatorInterface} from "./MyTranslatorInterface"; 3 | 4 | export class Translator { 5 | private logger: any; 6 | private translatorSettings: any; 7 | private translatorImplementation: undefined | MyTranslatorInterface; 8 | 9 | constructor(translatorSettings: any, logger: any) { 10 | this.logger = logger; 11 | this.translatorSettings = translatorSettings; 12 | } 13 | 14 | async init() { 15 | try { 16 | let auth_key = await this.getAuthKey(); 17 | await this.reloadAuthKey(auth_key); 18 | let correctObj = await this.getSettingsAuthKeyCorrectObject(); 19 | await this.setSettings(correctObj) 20 | } catch (error) { 21 | await this.setSettings(this.getSettingsAuthKeyErrorObject(error)); 22 | } 23 | } 24 | 25 | async translate(text: string, source_language: string, destination_language: string) { 26 | if(!this.translatorImplementation) return null; 27 | const translation = await this.translatorImplementation.translate(text, source_language, destination_language); 28 | await this.reloadUsage(); //update usage stats 29 | return translation; 30 | } 31 | 32 | async getSettingsAuthKeyCorrectObject() { 33 | const usage = await this.getUsage(); 34 | const extra = await this.getExtra(); 35 | return {valid_auth_key: true, informations: "Auth Key is valid!", ...usage, ...extra}; 36 | } 37 | 38 | getSettingsAuthKeyErrorObject(error: any) { 39 | return {auth_key: null, valid_auth_key: false, informations: "Auth Key not valid!\n" + error.toString()} 40 | } 41 | 42 | /** Private Methods */ 43 | 44 | async reloadAuthKey(auth_key: string) { 45 | this.translatorImplementation = new DeepLTranslator(auth_key); 46 | await this.translatorImplementation.init(); 47 | await this.reloadUsage(); 48 | } 49 | 50 | async reloadUsage() { 51 | const usage = await this.getUsage(); 52 | const used = usage.used || 0; 53 | const limit = usage.limit || 0; 54 | let percentage = 0; 55 | if (limit > 0) { 56 | percentage = Math.round((used / limit) * 100); 57 | } 58 | await this.setSettings({percentage: percentage, ...usage}); 59 | } 60 | 61 | async getUsage() { 62 | if(!this.translatorImplementation) return {used: 0, limit: 0}; 63 | return await this.translatorImplementation.getUsage(); 64 | } 65 | 66 | async getExtra() { 67 | if(!this.translatorImplementation) return {extra: ""}; 68 | return await this.translatorImplementation.getExtra(); 69 | } 70 | 71 | async setSettings(newSettings: any) { 72 | await this.translatorSettings.setSettings(newSettings); 73 | } 74 | 75 | async getAuthKey() { 76 | return await this.translatorSettings.getAuthKey(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/src/TranslatorSettings.ts: -------------------------------------------------------------------------------- 1 | import {ItemsServiceCreator} from './helper/ItemsServiceCreator.js'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const ENV_NAME_API_KEY = "AUTO_TRANSLATE_API_KEY"; 6 | const ENV_NAME_PATH_TO_SAVE_API_KEY = "AUTO_TRANSLATE_API_KEY_SAVING_PATH"; 7 | const API_KEY_PLACEHOLDER = "XXXXXXXXXXXXXXXXXXXXX"; 8 | 9 | const FIELDNAME_AUTH_KEY = "auth_key"; 10 | 11 | export class TranslatorSettings { 12 | 13 | static TABLENAME = "auto_translation_settings"; 14 | private database: any; 15 | private itemsServiceCreator: ItemsServiceCreator; 16 | private apiKey: null | string; 17 | private translationSettingsService: any; 18 | 19 | constructor(services: any, database: any, schema: any) { 20 | this.database = database; 21 | this.itemsServiceCreator = new ItemsServiceCreator(services, database, schema); 22 | this.apiKey = null; // To hold the API key in memory 23 | } 24 | 25 | async init(){ 26 | //console.log("INIT TranslatorSettings"); 27 | this.translationSettingsService = await this.itemsServiceCreator.getItemsService(TranslatorSettings.TABLENAME); 28 | 29 | // Load the API key from the file if the environment variable is set 30 | const apiKeyPath = process.env[ENV_NAME_PATH_TO_SAVE_API_KEY]; 31 | //console.log("API PATH: "+apiKeyPath); 32 | if (apiKeyPath) { 33 | try{ 34 | this.apiKey = fs.readFileSync(path.resolve(apiKeyPath), 'utf-8').trim(); 35 | //console.log("Found API key: "+this.apiKey) 36 | } catch (err){ 37 | //console.log("File not found yet. Will create it later") 38 | } 39 | } 40 | 41 | const apiKeyFromEnv = process.env[ENV_NAME_API_KEY]; 42 | if (apiKeyFromEnv) { 43 | this.apiKey = apiKeyFromEnv; 44 | } 45 | 46 | } 47 | 48 | saveApiKeySecureIfConfiguredAndReturnPayload(payload: any) { 49 | const apiKeyPath = process.env[ENV_NAME_PATH_TO_SAVE_API_KEY]; 50 | 51 | let newApiKey = payload[FIELDNAME_AUTH_KEY]; 52 | //console.log("new API key: " + newApiKey); 53 | 54 | if (apiKeyPath && newApiKey) { 55 | let filePath = path.resolve(apiKeyPath); 56 | let dirName = path.dirname(filePath); 57 | 58 | // Check if the directory exists; if not, create it 59 | if (!fs.existsSync(dirName)) { 60 | fs.mkdirSync(dirName, { recursive: true }); 61 | //console.log("Created directory: " + dirName); 62 | } 63 | 64 | //console.log("Saving to file"); 65 | fs.writeFileSync(filePath, newApiKey, 'utf-8'); 66 | 67 | // Update the in-memory apiKey with the new value 68 | this.apiKey = newApiKey; 69 | 70 | // Replace the API key with a placeholder before saving to the database 71 | payload[FIELDNAME_AUTH_KEY] = API_KEY_PLACEHOLDER; 72 | } 73 | 74 | return payload; 75 | } 76 | 77 | 78 | async setSettings(newSettings: any) { 79 | let settings = await this.getSettings(); 80 | if(!!settings && settings?.id){ 81 | await this.translationSettingsService.updateOne(settings?.id, newSettings); 82 | } 83 | } 84 | 85 | async getSettings() { 86 | // on creating an item, we cant use knex? 87 | // KnexTimeoutError: Knex: Timeout acquiring a connection. The pool is probably full. 88 | let settings = await this.translationSettingsService.readByQuery({}); 89 | if(!!settings && settings.length > 0){ 90 | let settingsToReturn = settings[0]; 91 | return settingsToReturn; 92 | } 93 | return null; 94 | } 95 | 96 | async isAutoTranslationEnabled() { 97 | let settings = await this.getSettings(); 98 | return settings?.active; 99 | } 100 | 101 | async getAuthKey() { 102 | return this.apiKey; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Discontinoued

2 | 3 | This project is discontinued. We are not able to maintain this project anymore, as we are integrating it directly into our software. If you want to take over this project, please contact us. 4 | 5 |

6 | Directus Extension Auto Translation 7 |

8 |

9 | drawing 10 |

11 | 12 |

13 | npm package 14 | MIT 15 | last commit 16 | downloads week 17 | downloads total 18 | size 19 | size 20 |

21 | 22 |

23 | directus-extension-auto-translation 24 |

25 | 26 | ### About 27 | 28 | This extension automatically translates Directus collections translation fields. This will be achieved by DeepL integration. 29 | 30 | With a free DeepL account you can translate 500.000 words per month free. 31 | 32 | 33 | ### Requirements 34 | 35 | - DeepL Auth-Key (free or pro) 36 | - https://www.deepl.com/de/docs-api/api-access/authentication/ 37 | 38 | ### Installation 39 | 40 | - https://docs.directus.io/extensions/installing-extensions.html 41 | 42 | 1. Backup your database! 43 | 2. Installing via the npm Registry 44 | - Dockerfile 45 | ``` 46 | RUN pnpm install directus-extension-auto-translation 47 | ``` 48 | 3. [Recommended] 49 | - Disable saving API key into database. 50 | - a) 51 | - Add the `env` Variable: `AUTO_TRANSLATE_API_KEY_SAVING_PATH` which holds a path 52 | - Since saving an API key in the database is never a good idea. This allows us, to save the Key into a File. 53 | - This allows your customers to dynamically change the API key. 54 | - setup: 55 | - volumes: 56 | - ```- ./secrets:/directus/secrets``` 57 | - env section: 58 | - ```AUTO_TRANSLATE_API_KEY_SAVING_PATH: "/directus/secrets/api-key-in-file.txt"``` 59 | - b) 60 | - Add the `env` Variable: `AUTO_TRANSLATE_API_KEY` which holds the api key 61 | - This does not allow dynamically changing the API key as in option a) 62 | 63 | 4. Follow the instructions in your Directus App add the new created table (`auto_translation_settings`) 64 | 65 | ### Usage 66 | This example shows how to use the extension for a collection `wikis` 67 | 68 | 1. Add a `translation` type field to your collection 69 | 2. Directus automatically creates a `wikis_translations` and `languages` collection 70 | 3. In this translation collection (`wikis_translations`) 71 | - Add a `be_source_for_translations` field (default: `true`) 72 | - This field is used to determine if the translation is the source of the translation 73 | - Add a `let_be_translated` field (default: `true`) 74 | - This field is used to determine if the record should be translated 75 | 76 | If you now create or update a record in the `wikis` collection with a `translation` it will be automatically translated. 77 | 78 | ## Contributors 79 | 80 | The FireboltCasters 81 | 82 | Contributors 83 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/src/DeepLTranslator.ts: -------------------------------------------------------------------------------- 1 | import deepl, {SourceLanguageCode, TargetLanguageCode, Translator} from 'deepl-node'; 2 | import {MyTranslatorInterface} from "./MyTranslatorInterface"; 3 | 4 | 5 | export class DeepLTranslator implements MyTranslatorInterface { 6 | private translator: Translator; 7 | 8 | constructor(auth_key: string) { 9 | this.translator = new deepl.Translator(auth_key); 10 | } 11 | 12 | async init(){ 13 | /** 14 | console.log("Initializing DeepL Translator"); 15 | const sourceLanguages = await this.translator.getSourceLanguages(); 16 | console.log("Source Languages: "); 17 | for (let i = 0; i < sourceLanguages.length; i++) { 18 | const lang = sourceLanguages[i]; 19 | console.log(`${lang.name} (${lang.code})`); // Example: 'English (en)' 20 | } 21 | 22 | console.log(""); 23 | const targetLanguages = await this.translator.getTargetLanguages(); 24 | console.log("Target Languages: "); 25 | for (let i = 0; i < targetLanguages.length; i++) { 26 | const lang = targetLanguages[i]; 27 | console.log(`${lang.name} (${lang.code}) supports formality`); 28 | } 29 | */ 30 | } 31 | 32 | async translate(text: string, source_language: string, destination_language: string) { 33 | let translationResponse = null; 34 | let sourceLanguageCode = this.getDeepLLanguageCodeSource(source_language); 35 | let destinationLanguageCode = this.getDeepLLanguageCodeTarget(destination_language); 36 | 37 | try{ 38 | translationResponse = await this.translateRaw(text, sourceLanguageCode, destinationLanguageCode); 39 | } catch(error: any){ 40 | let errorMessage = error.toString(); 41 | if(errorMessage.includes("targetLang") && errorMessage.includes("deprecated")){ 42 | //console.log("Target language is deprecated"); 43 | try{ 44 | let backupDestinationLanguageCode = destination_language as TargetLanguageCode; 45 | translationResponse = await this.translateRaw(text, sourceLanguageCode, backupDestinationLanguageCode); 46 | } catch(error){ 47 | console.log(error); 48 | } 49 | } else { 50 | console.log(error); 51 | } 52 | } 53 | 54 | return translationResponse; 55 | } 56 | 57 | private replaceAll(str: string, find: string, replace: string) { 58 | // use regex where find is replaced with replace globally and multiple times 59 | // find could be a special character like * which needs to be escaped 60 | return str.replace(new RegExp(find.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'), replace); 61 | } 62 | 63 | async translateRaw(text: string, source_language_code: SourceLanguageCode, destination_language_code: TargetLanguageCode) { 64 | //copy text string to another variable 65 | let textToTranslate: string = text; 66 | 67 | const dictWithReplacement = { 68 | // "original": "replacement" 69 | // replace * with <*> 70 | "*": "<*>", 71 | } 72 | 73 | //replace all keys in dictWithReplacement with their values 74 | for (const [key, value] of Object.entries(dictWithReplacement)) { 75 | textToTranslate = this.replaceAll(textToTranslate, key, value) 76 | } 77 | 78 | //console.log("translate:") 79 | //console.log("text: "+text); 80 | //console.log("source_language_code: "+source_language_code) 81 | //console.log("destination_language_code: "+destination_language_code) 82 | 83 | let translationResponse = await this.translator.translateText(textToTranslate, source_language_code, destination_language_code); 84 | let translation = translationResponse?.text; 85 | 86 | //replace all values in dictWithReplacement with their keys 87 | for (const [key, value] of Object.entries(dictWithReplacement)) { 88 | translation = this.replaceAll(translation, value, key) 89 | } 90 | 91 | //replace all <*>'s with *'s 92 | 93 | return translation; 94 | } 95 | 96 | async getExtra(){ 97 | 98 | const sourceLanguages = await this.translator.getSourceLanguages() 99 | const targetLanguages = await this.translator.getTargetLanguages() 100 | 101 | let extraObj = { 102 | sourceLanguages: sourceLanguages, 103 | targetLanguages: targetLanguages 104 | }; 105 | const extra = JSON.stringify(extraObj, null, 2); 106 | 107 | return { 108 | extra: extra || "", 109 | } 110 | } 111 | 112 | async getUsage(){ 113 | const usage = await this.translator.getUsage(); 114 | if (usage.anyLimitReached()) { 115 | console.log('Translation limit exceeded.'); 116 | } 117 | const characterUsage = usage?.character; // {"character":{"count":0,"limit":500000}} 118 | 119 | return { 120 | used: characterUsage?.count || 0, 121 | limit: characterUsage?.limit || 0, 122 | } 123 | } 124 | 125 | /** 126 | * Private Methods 127 | */ 128 | 129 | getDeepLLanguageCodeSource(directus_language_code: string){ 130 | return this.getDeepLLanguageCode(directus_language_code) as SourceLanguageCode; 131 | } 132 | 133 | getDeepLLanguageCodeTarget(directus_language_code: string){ 134 | return this.getDeepLLanguageCode(directus_language_code) as TargetLanguageCode; 135 | } 136 | 137 | getDeepLLanguageCode(directus_language_code: string){ 138 | /** directus_language_code 139 | * e.g. "en-US" -> "en" 140 | */ 141 | 142 | /** Source languages 143 | Bulgarian (bg) 144 | Czech (cs) 145 | Danish (da) 146 | German (de) 147 | Greek (el) 148 | English (en) 149 | Spanish (es) 150 | Estonian (et) 151 | Finnish (fi) 152 | French (fr) 153 | Hungarian (hu) 154 | Indonesian (id) 155 | Italian (it) 156 | Japanese (ja) 157 | Lithuanian (lt) 158 | Latvian (lv) 159 | Dutch (nl) 160 | Polish (pl) 161 | Portuguese (pt) 162 | Romanian (ro) 163 | Russian (ru) 164 | Slovak (sk) 165 | Slovenian (sl) 166 | Swedish (sv) 167 | Turkish (tr) 168 | Chinese (zh) 169 | */ 170 | 171 | /** Target languages 172 | Bulgarian (bg) supports formality 173 | Czech (cs) supports formality 174 | Danish (da) supports formality 175 | German (de) supports formality 176 | Greek (el) supports formality 177 | English (British) (en-GB) supports formality 178 | English (American) (en-US) supports formality 179 | Spanish (es) supports formality 180 | Estonian (et) supports formality 181 | Finnish (fi) supports formality 182 | French (fr) supports formality 183 | Hungarian (hu) supports formality 184 | Indonesian (id) supports formality 185 | Italian (it) supports formality 186 | Japanese (ja) supports formality 187 | Lithuanian (lt) supports formality 188 | Latvian (lv) supports formality 189 | Dutch (nl) supports formality 190 | Polish (pl) supports formality 191 | Portuguese (Brazilian) (pt-BR) supports formality 192 | Portuguese (European) (pt-PT) supports formality 193 | Romanian (ro) supports formality 194 | Russian (ru) supports formality 195 | Slovak (sk) supports formality 196 | Slovenian (sl) supports formality 197 | Swedish (sv) supports formality 198 | Turkish (tr) supports formality 199 | Chinese (simplified) (zh) supports formality 200 | */ 201 | 202 | if(!!directus_language_code){ 203 | let splits = directus_language_code.split("-"); 204 | return splits[0]; 205 | } 206 | 207 | return directus_language_code; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/src/index.ts: -------------------------------------------------------------------------------- 1 | import { defineHook } from '@directus/extensions-sdk'; 2 | 3 | import {ItemsServiceCreator} from './helper/ItemsServiceCreator.js'; 4 | import {CollectionsServiceCreator} from './helper/CollectionsServiceCreator.js'; 5 | import {Translator} from './Translator'; 6 | import {TranslatorSettings} from './TranslatorSettings'; 7 | import {DirectusCollectionTranslator} from './DirectusCollectionTranslator.js'; 8 | import getSettingsSchema from "./schema/schema"; 9 | import yaml from "js-yaml" 10 | import packageJson from "../package.json" 11 | const PLUGIN_NAME = packageJson.name; 12 | 13 | const settingsSchemaYAML = getSettingsSchema(); 14 | const settingsSchema = yaml.load(settingsSchemaYAML); 15 | 16 | const DEV_MODE = false 17 | 18 | async function getAndInitItemsServiceCreatorAndTranslatorSettingsAndTranslatorAndSchema(services, database, getSchema, logger) { 19 | let schema = await getSchema(); 20 | let itemsServiceCreator = new ItemsServiceCreator(services, database, schema); 21 | let translatorSettings = new TranslatorSettings(services, database, schema); 22 | await translatorSettings.init(); 23 | let translator = new Translator(translatorSettings, logger); 24 | await translator.init(); 25 | return { 26 | itemsServiceCreator: itemsServiceCreator, 27 | translatorSettings: translatorSettings, 28 | translator: translator, 29 | schema: schema 30 | } 31 | } 32 | 33 | async function getCurrentItemForTranslation(itemsService, meta, translations_field) { 34 | //console.log("getCurrentItemForTranslation"); 35 | let currentItem = {}; //For create we don't have a current item 36 | let primaryKeys = meta?.keys || []; 37 | for (let primaryKey of primaryKeys) { //For update we have a current item 38 | currentItem = await itemsService.readOne(primaryKey, {fields: [translations_field+".*"]}); 39 | break; //we only need get the first primary key 40 | } 41 | return currentItem; 42 | } 43 | 44 | async function handleCreateOrUpdate(tablename, payload, meta, context, getSchema, services, logger) { 45 | if(tablename === TranslatorSettings.TABLENAME){ 46 | // Don't translate settings 47 | return payload; 48 | } 49 | 50 | //console.log("handleCreateOrUpdate"); 51 | //console.log("Table: "+tablename); 52 | //console.log("Payload: "); 53 | //console.log(JSON.stringify(payload, null, 2)); 54 | let database = context.database; //Have to get database here! https://github.com/directus/directus/discussions/13744 55 | let schemaToGetTranslationFields = await getSchema(); 56 | 57 | let field_special_translation = "translations"; 58 | let table_schema = schemaToGetTranslationFields.collections[tablename]; 59 | // { 60 | // "collection": "singletonExample", 61 | // ... 62 | // "fields": { 63 | // ... 64 | // "translations": { 65 | // "field": "translations", 66 | // "defaultValue": null, 67 | // "nullable": true, 68 | // "generated": false, 69 | // "type": "alias", 70 | // "dbType": null, 71 | // "precision": null, 72 | // "scale": null, 73 | // "special": [ 74 | // "translations" 75 | // ], 76 | // } 77 | // } 78 | // } 79 | let schema_fields = table_schema.fields; 80 | // search for all fields which are from type "special" and have "translations" in special array 81 | let translations_fields = Object.keys(schema_fields).filter(field => schema_fields[field].special?.includes(field_special_translation)); 82 | 83 | let payloadContainsTranslations = false; 84 | for(let translations_field of translations_fields){ 85 | if(payload[translations_field] !== undefined){ 86 | payloadContainsTranslations = true; 87 | break; 88 | } 89 | } 90 | //console.log("Payload contains translations: "+payloadContainsTranslations); 91 | if (payloadContainsTranslations) { 92 | let { 93 | itemsServiceCreator, 94 | translatorSettings, 95 | translator, 96 | schema 97 | } = await getAndInitItemsServiceCreatorAndTranslatorSettingsAndTranslatorAndSchema(services, database, getSchema, logger); 98 | 99 | let autoTranslate = await translatorSettings.isAutoTranslationEnabled(); 100 | if (autoTranslate || DEV_MODE) { 101 | //console.log("Auto-Translation enabled for "+tablename+" table (DEV_MODE: "+DEV_MODE+")"); 102 | let itemsService = await itemsServiceCreator.getItemsService(tablename); 103 | //console.log("Table schema: "); 104 | //console.log(JSON.stringify(table_schema, null, 2)); 105 | //console.log("Translations fields: "); 106 | //console.log(translations_fields); 107 | 108 | let modifiedPayload = payload; 109 | //console.log("["+PLUGIN_NAME+"] - "+"Start translation for "+tablename+" table"); 110 | for(let translations_field of translations_fields){ 111 | let currentItem = await getCurrentItemForTranslation(itemsService, meta, translations_field); 112 | modifiedPayload = await DirectusCollectionTranslator.modifyPayloadForTranslation(currentItem, modifiedPayload, translator, translatorSettings, itemsServiceCreator, schema, tablename, translations_field); 113 | } 114 | //console.log("["+PLUGIN_NAME+"] - "+"End translation for "+tablename+" table"); 115 | 116 | return modifiedPayload; 117 | } 118 | } 119 | return payload; 120 | } 121 | 122 | function registerCollectionAutoTranslation(filter, getSchema, services, logger) { 123 | let events = ["create", "update"]; 124 | for (let event of events) { 125 | filter( 126 | "items." + event, 127 | async (payload, meta, context) => { 128 | let tablename = meta?.collection; 129 | //console.log("Auto-Translation for "+event+" event in "+tablename); 130 | return await handleCreateOrUpdate(tablename, payload, meta, context, getSchema, services, logger); 131 | } 132 | ); 133 | } 134 | } 135 | 136 | async function registerAuthKeyReloader(filter, translator) { 137 | filter( 138 | TranslatorSettings.TABLENAME + ".items.update", 139 | async (payload, meta, context) => { 140 | if (payload?.auth_key !== undefined) { // Auth Key changed 141 | try { 142 | //console.log("registerAuthKeyReloader"); 143 | await translator.reloadAuthKey(payload?.auth_key); //Try to reload auth key 144 | //console.log("Censoring api key not") 145 | const censoredPayload = await translator.translatorSettings.saveApiKeySecureIfConfiguredAndReturnPayload(payload) 146 | const correctObj = await translator.getSettingsAuthKeyCorrectObject(); // 147 | 148 | payload = {...censoredPayload, ...correctObj}; //Set settings to valid 149 | //console.log("Final payload at: registerAuthKeyReloader"); 150 | //console.log(JSON.stringify(payload, null, 2)) 151 | 152 | } catch (err) { //Auth Key not valid 153 | payload = {...payload, ...translator.getSettingsAuthKeyErrorObject(err)}; 154 | } 155 | } 156 | return payload; 157 | } 158 | ); 159 | } 160 | 161 | async function checkSettingsCollection(services, database, schema) { 162 | let collectionsServiceCreator = new CollectionsServiceCreator(services, database, schema); 163 | let collectionsService = await collectionsServiceCreator.getCollectionsService(); 164 | try { 165 | let collections = await collectionsService.readByQuery(); //no query params possible ! 166 | let collectionFound = false; 167 | for (let collection of collections) { 168 | if (collection.collection === TranslatorSettings.TABLENAME) { 169 | collectionFound = true; 170 | break; 171 | } 172 | } 173 | if (!collectionFound) { 174 | console.log("Collection "+TranslatorSettings.TABLENAME+" not found"); 175 | let settingsSchemaCollection = settingsSchema.collections[0]; 176 | let settingsSchemaFields = settingsSchema.fields; 177 | 178 | console.log("Creating "+TranslatorSettings.TABLENAME+" collection"); 179 | await collectionsService.createOne({ 180 | ...settingsSchemaCollection, 181 | fields: settingsSchemaFields 182 | }); 183 | console.log("Created "+TranslatorSettings.TABLENAME+" collection"); 184 | } else { 185 | //console.log("Settings collection found"); 186 | } 187 | 188 | } catch (err) { 189 | console.log(err); 190 | } 191 | } 192 | 193 | export default defineHook(({filter, action, init, schedule}, { 194 | services, 195 | database, 196 | getSchema, 197 | logger 198 | }) => { 199 | action( 200 | "server.start", 201 | async (meta, context) => { 202 | try{ 203 | let schema = await getSchema(); 204 | console.log("Loading Plugin") 205 | await checkSettingsCollection(services, database, schema) 206 | 207 | let translatorSettings = new TranslatorSettings(services, database, schema); 208 | await translatorSettings.init(); 209 | let translator = new Translator(translatorSettings, logger); 210 | await translator.init(); 211 | await registerAuthKeyReloader(filter, translator); 212 | 213 | registerCollectionAutoTranslation(filter, getSchema, services, logger); 214 | //registerLanguagesFilter(filter, getSchema, services, logger); //TODO implement auto translate for new languages 215 | } catch (err) { 216 | let errMsg = err.toString(); 217 | if(errMsg.includes("no such table: directus_collections")){ 218 | console.log("++++++++++ Auto Translation +++++++++++"); 219 | console.log("++++ Database not initialized yet +++++"); 220 | console.log("++ Restart Server again after setup +++"); 221 | console.log("+++++++++++++++++++++++++++++++++++++++"); 222 | } else { 223 | console.log("Auto-Translation init error: "); 224 | console.log(err); 225 | } 226 | } 227 | } 228 | ) 229 | }); 230 | -------------------------------------------------------------------------------- /dev/scheme.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | directus: 9.18.1 3 | collections: 4 | - collection: auto_translation_settings 5 | meta: 6 | accountability: all 7 | archive_app_filter: true 8 | archive_field: null 9 | archive_value: null 10 | collapse: open 11 | collection: auto_translation_settings 12 | color: null 13 | display_template: null 14 | group: null 15 | hidden: false 16 | icon: null 17 | item_duplication_fields: null 18 | note: null 19 | singleton: true 20 | sort: 2 21 | sort_field: null 22 | translations: null 23 | unarchive_value: null 24 | schema: 25 | name: auto_translation_settings 26 | sql: >- 27 | CREATE TABLE `auto_translation_settings` (`active` boolean null default 28 | '1', `auth_key` varchar(255) null default null, `id` integer not null 29 | primary key autoincrement, `informations` text null default null, 30 | `limit` integer null default '500000', `percentage` integer null default 31 | null, `used` integer null default '0', `valid_auth_key` boolean null 32 | default '0', `extra` text null default null) 33 | - collection: test 34 | meta: 35 | accountability: all 36 | archive_app_filter: true 37 | archive_field: null 38 | archive_value: null 39 | collapse: open 40 | collection: test 41 | color: null 42 | display_template: null 43 | group: null 44 | hidden: false 45 | icon: null 46 | item_duplication_fields: null 47 | note: null 48 | singleton: false 49 | sort: null 50 | sort_field: null 51 | translations: null 52 | unarchive_value: null 53 | schema: 54 | name: test 55 | sql: >- 56 | CREATE TABLE `test` (`id` integer not null primary key autoincrement, 57 | `name` varchar(255) null) 58 | fields: 59 | - collection: auto_translation_settings 60 | field: active 61 | meta: 62 | collection: auto_translation_settings 63 | conditions: 64 | - name: 'false' 65 | rule: 66 | _and: 67 | - valid_auth_key: 68 | _eq: false 69 | options: 70 | iconOn: check_box 71 | iconOff: check_box_outline_blank 72 | label: Enabled 73 | display: null 74 | display_options: null 75 | field: active 76 | group: visible_for_valid_auth_key 77 | hidden: false 78 | interface: boolean 79 | note: null 80 | options: null 81 | readonly: false 82 | required: false 83 | sort: 2 84 | special: 85 | - cast-boolean 86 | translations: null 87 | validation: null 88 | validation_message: null 89 | width: full 90 | schema: 91 | data_type: boolean 92 | default_value: true 93 | foreign_key_column: null 94 | foreign_key_table: null 95 | generation_expression: null 96 | has_auto_increment: false 97 | is_generated: false 98 | is_nullable: true 99 | is_primary_key: false 100 | is_unique: false 101 | max_length: null 102 | name: active 103 | numeric_precision: null 104 | numeric_scale: null 105 | table: auto_translation_settings 106 | type: boolean 107 | - collection: auto_translation_settings 108 | field: auth_key 109 | meta: 110 | collection: auto_translation_settings 111 | conditions: null 112 | display: null 113 | display_options: null 114 | field: auth_key 115 | group: null 116 | hidden: false 117 | interface: input 118 | note: >- 119 | Authentication - You need an authentication key to access to the API. 120 | https://www.deepl.com/de/account/summary 121 | options: 122 | iconLeft: key 123 | masked: true 124 | readonly: false 125 | required: false 126 | sort: 4 127 | special: null 128 | translations: null 129 | validation: null 130 | validation_message: null 131 | width: full 132 | schema: 133 | data_type: varchar 134 | default_value: null 135 | foreign_key_column: null 136 | foreign_key_table: null 137 | generation_expression: null 138 | has_auto_increment: false 139 | is_generated: false 140 | is_nullable: true 141 | is_primary_key: false 142 | is_unique: false 143 | max_length: 255 144 | name: auth_key 145 | numeric_precision: null 146 | numeric_scale: null 147 | table: auto_translation_settings 148 | type: string 149 | - collection: auto_translation_settings 150 | field: extra 151 | meta: 152 | collection: auto_translation_settings 153 | conditions: null 154 | display: null 155 | display_options: null 156 | field: extra 157 | group: visible_for_valid_auth_key 158 | hidden: false 159 | interface: input-multiline 160 | note: Informations about errors will be shown here. 161 | options: null 162 | readonly: true 163 | required: false 164 | sort: 4 165 | special: null 166 | translations: null 167 | validation: null 168 | validation_message: null 169 | width: full 170 | schema: 171 | data_type: text 172 | default_value: null 173 | foreign_key_column: null 174 | foreign_key_table: null 175 | generation_expression: null 176 | has_auto_increment: false 177 | is_generated: false 178 | is_nullable: true 179 | is_primary_key: false 180 | is_unique: false 181 | max_length: null 182 | name: extra 183 | numeric_precision: null 184 | numeric_scale: null 185 | table: auto_translation_settings 186 | type: text 187 | - collection: auto_translation_settings 188 | field: id 189 | meta: 190 | collection: auto_translation_settings 191 | conditions: null 192 | display: null 193 | display_options: null 194 | field: id 195 | group: null 196 | hidden: true 197 | interface: input 198 | note: null 199 | options: null 200 | readonly: true 201 | required: false 202 | sort: 1 203 | special: null 204 | translations: null 205 | validation: null 206 | validation_message: null 207 | width: full 208 | schema: 209 | data_type: integer 210 | default_value: null 211 | foreign_key_column: null 212 | foreign_key_table: null 213 | generation_expression: null 214 | has_auto_increment: true 215 | is_generated: false 216 | is_nullable: false 217 | is_primary_key: true 218 | is_unique: false 219 | max_length: null 220 | name: id 221 | numeric_precision: null 222 | numeric_scale: null 223 | table: auto_translation_settings 224 | type: integer 225 | - collection: auto_translation_settings 226 | field: informations 227 | meta: 228 | collection: auto_translation_settings 229 | conditions: null 230 | display: null 231 | display_options: null 232 | field: informations 233 | group: null 234 | hidden: false 235 | interface: input-multiline 236 | note: Informations about errors will be shown here. 237 | options: null 238 | readonly: true 239 | required: false 240 | sort: 3 241 | special: null 242 | translations: null 243 | validation: null 244 | validation_message: null 245 | width: full 246 | schema: 247 | data_type: text 248 | default_value: null 249 | foreign_key_column: null 250 | foreign_key_table: null 251 | generation_expression: null 252 | has_auto_increment: false 253 | is_generated: false 254 | is_nullable: true 255 | is_primary_key: false 256 | is_unique: false 257 | max_length: null 258 | name: informations 259 | numeric_precision: null 260 | numeric_scale: null 261 | table: auto_translation_settings 262 | type: text 263 | - collection: auto_translation_settings 264 | field: limit 265 | meta: 266 | collection: auto_translation_settings 267 | conditions: null 268 | display: null 269 | display_options: null 270 | field: limit 271 | group: usage 272 | hidden: false 273 | interface: input 274 | note: null 275 | options: null 276 | readonly: true 277 | required: false 278 | sort: 3 279 | special: null 280 | translations: null 281 | validation: null 282 | validation_message: null 283 | width: half 284 | schema: 285 | data_type: integer 286 | default_value: 500000 287 | foreign_key_column: null 288 | foreign_key_table: null 289 | generation_expression: null 290 | has_auto_increment: false 291 | is_generated: false 292 | is_nullable: true 293 | is_primary_key: false 294 | is_unique: false 295 | max_length: null 296 | name: limit 297 | numeric_precision: null 298 | numeric_scale: null 299 | table: auto_translation_settings 300 | type: integer 301 | - collection: auto_translation_settings 302 | field: notice 303 | meta: 304 | collection: auto_translation_settings 305 | conditions: null 306 | display: null 307 | display_options: null 308 | field: notice 309 | group: visible_for_valid_auth_key 310 | hidden: false 311 | interface: presentation-notice 312 | note: null 313 | options: 314 | text: >- 315 | If you want a collection (e. G. wikis) to be translated do the 316 | following. Add a field type "translations" which will create a new 317 | collection (e. G. wikis_translations). In this collection add the 318 | following boolean (default: true) fields: 319 | "be_source_for_translations", "let_be_translated" and 320 | "create_translations_for_all_languages". Ensure that Directus 321 | automatically created a collection "languages". 322 | readonly: false 323 | required: false 324 | sort: 1 325 | special: 326 | - alias 327 | - no-data 328 | translations: null 329 | validation: null 330 | validation_message: null 331 | width: full 332 | schema: null 333 | type: alias 334 | - collection: auto_translation_settings 335 | field: percentage 336 | meta: 337 | collection: auto_translation_settings 338 | conditions: null 339 | display: formatted-value 340 | display_options: 341 | suffix: ' %' 342 | field: percentage 343 | group: usage 344 | hidden: false 345 | interface: slider 346 | note: null 347 | options: 348 | alwaysShowValue: true 349 | maxValue: 100 350 | minValue: 0 351 | readonly: true 352 | required: false 353 | sort: 1 354 | special: null 355 | translations: null 356 | validation: null 357 | validation_message: null 358 | width: full 359 | schema: 360 | data_type: integer 361 | default_value: null 362 | foreign_key_column: null 363 | foreign_key_table: null 364 | generation_expression: null 365 | has_auto_increment: false 366 | is_generated: false 367 | is_nullable: true 368 | is_primary_key: false 369 | is_unique: false 370 | max_length: null 371 | name: percentage 372 | numeric_precision: null 373 | numeric_scale: null 374 | table: auto_translation_settings 375 | type: integer 376 | - collection: auto_translation_settings 377 | field: usage 378 | meta: 379 | collection: auto_translation_settings 380 | conditions: null 381 | display: null 382 | display_options: null 383 | field: usage 384 | group: visible_for_valid_auth_key 385 | hidden: false 386 | interface: group-raw 387 | note: null 388 | options: null 389 | readonly: false 390 | required: false 391 | sort: 3 392 | special: 393 | - alias 394 | - group 395 | - no-data 396 | translations: null 397 | validation: null 398 | validation_message: null 399 | width: full 400 | schema: null 401 | type: alias 402 | - collection: auto_translation_settings 403 | field: used 404 | meta: 405 | collection: auto_translation_settings 406 | conditions: null 407 | display: raw 408 | display_options: null 409 | field: used 410 | group: usage 411 | hidden: false 412 | interface: input 413 | note: null 414 | options: null 415 | readonly: false 416 | required: false 417 | sort: 2 418 | special: null 419 | translations: null 420 | validation: null 421 | validation_message: null 422 | width: half 423 | schema: 424 | data_type: integer 425 | default_value: 0 426 | foreign_key_column: null 427 | foreign_key_table: null 428 | generation_expression: null 429 | has_auto_increment: false 430 | is_generated: false 431 | is_nullable: true 432 | is_primary_key: false 433 | is_unique: false 434 | max_length: null 435 | name: used 436 | numeric_precision: null 437 | numeric_scale: null 438 | table: auto_translation_settings 439 | type: integer 440 | - collection: auto_translation_settings 441 | field: valid_auth_key 442 | meta: 443 | collection: auto_translation_settings 444 | conditions: null 445 | display: null 446 | display_options: null 447 | field: valid_auth_key 448 | group: null 449 | hidden: true 450 | interface: boolean 451 | note: null 452 | options: null 453 | readonly: true 454 | required: false 455 | sort: 2 456 | special: 457 | - cast-boolean 458 | translations: null 459 | validation: null 460 | validation_message: null 461 | width: full 462 | schema: 463 | data_type: boolean 464 | default_value: false 465 | foreign_key_column: null 466 | foreign_key_table: null 467 | generation_expression: null 468 | has_auto_increment: false 469 | is_generated: false 470 | is_nullable: true 471 | is_primary_key: false 472 | is_unique: false 473 | max_length: null 474 | name: valid_auth_key 475 | numeric_precision: null 476 | numeric_scale: null 477 | table: auto_translation_settings 478 | type: boolean 479 | - collection: auto_translation_settings 480 | field: visible_for_valid_auth_key 481 | meta: 482 | collection: auto_translation_settings 483 | conditions: 484 | - rule: 485 | _and: 486 | - valid_auth_key: 487 | _eq: false 488 | readonly: true 489 | hidden: true 490 | options: {} 491 | display: null 492 | display_options: null 493 | field: visible_for_valid_auth_key 494 | group: null 495 | hidden: false 496 | interface: group-raw 497 | note: null 498 | options: null 499 | readonly: false 500 | required: false 501 | sort: 5 502 | special: 503 | - alias 504 | - group 505 | - no-data 506 | translations: null 507 | validation: null 508 | validation_message: null 509 | width: full 510 | schema: null 511 | type: alias 512 | - collection: test 513 | field: id 514 | meta: 515 | collection: test 516 | conditions: null 517 | display: null 518 | display_options: null 519 | field: id 520 | group: null 521 | hidden: true 522 | interface: input 523 | note: null 524 | options: null 525 | readonly: true 526 | required: false 527 | sort: null 528 | special: null 529 | translations: null 530 | validation: null 531 | validation_message: null 532 | width: full 533 | schema: 534 | data_type: integer 535 | default_value: null 536 | foreign_key_column: null 537 | foreign_key_table: null 538 | generation_expression: null 539 | has_auto_increment: true 540 | is_generated: false 541 | is_nullable: false 542 | is_primary_key: true 543 | is_unique: false 544 | max_length: null 545 | name: id 546 | numeric_precision: null 547 | numeric_scale: null 548 | table: test 549 | type: integer 550 | - collection: test 551 | field: name 552 | meta: 553 | collection: test 554 | conditions: null 555 | display: null 556 | display_options: null 557 | field: name 558 | group: null 559 | hidden: false 560 | interface: input 561 | note: null 562 | options: null 563 | readonly: false 564 | required: false 565 | sort: null 566 | special: null 567 | translations: null 568 | validation: null 569 | validation_message: null 570 | width: full 571 | schema: 572 | data_type: varchar 573 | default_value: null 574 | foreign_key_column: null 575 | foreign_key_table: null 576 | generation_expression: null 577 | has_auto_increment: false 578 | is_generated: false 579 | is_nullable: true 580 | is_primary_key: false 581 | is_unique: false 582 | max_length: 255 583 | name: name 584 | numeric_precision: null 585 | numeric_scale: null 586 | table: test 587 | type: string 588 | relations: [] 589 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/src/schema/schema.ts: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return 'version: 1\n' + 3 | 'directus: 9.18.1\n' + 4 | 'collections:\n' + 5 | ' - collection: auto_translation_settings\n' + 6 | ' meta:\n' + 7 | ' accountability: all\n' + 8 | ' archive_app_filter: true\n' + 9 | ' archive_field: null\n' + 10 | ' archive_value: null\n' + 11 | ' collapse: open\n' + 12 | ' collection: auto_translation_settings\n' + 13 | ' color: null\n' + 14 | ' display_template: null\n' + 15 | ' group: null\n' + 16 | ' hidden: false\n' + 17 | ' icon: null\n' + 18 | ' item_duplication_fields: null\n' + 19 | ' note: null\n' + 20 | ' singleton: true\n' + 21 | ' sort: 2\n' + 22 | ' sort_field: null\n' + 23 | ' translations: null\n' + 24 | ' unarchive_value: null\n' + 25 | ' schema:\n' + 26 | ' name: auto_translation_settings\n' + 27 | ' sql: >-\n' + 28 | ' CREATE TABLE `auto_translation_settings` (`active` boolean null default\n' + 29 | ' \'1\', `auth_key` varchar(255) null default null, `id` integer not null\n' + 30 | ' primary key autoincrement, `informations` text null default null,\n' + 31 | ' `limit` integer null default \'500000\', `percentage` integer null default\n' + 32 | ' null, `used` integer null default \'0\', `valid_auth_key` boolean null\n' + 33 | ' default \'0\', `extra` text null default null)\n' + 34 | 'fields:\n' + 35 | ' - collection: auto_translation_settings\n' + 36 | ' field: active\n' + 37 | ' meta:\n' + 38 | ' collection: auto_translation_settings\n' + 39 | ' conditions:\n' + 40 | ' - name: \'false\'\n' + 41 | ' rule:\n' + 42 | ' _and:\n' + 43 | ' - valid_auth_key:\n' + 44 | ' _eq: false\n' + 45 | ' options:\n' + 46 | ' iconOn: check_box\n' + 47 | ' iconOff: check_box_outline_blank\n' + 48 | ' label: Enabled\n' + 49 | ' display: null\n' + 50 | ' display_options: null\n' + 51 | ' field: active\n' + 52 | ' group: visible_for_valid_auth_key\n' + 53 | ' hidden: false\n' + 54 | ' interface: boolean\n' + 55 | ' note: null\n' + 56 | ' options: null\n' + 57 | ' readonly: false\n' + 58 | ' required: false\n' + 59 | ' sort: 2\n' + 60 | ' special:\n' + 61 | ' - cast-boolean\n' + 62 | ' translations: null\n' + 63 | ' validation: null\n' + 64 | ' validation_message: null\n' + 65 | ' width: full\n' + 66 | ' schema:\n' + 67 | ' data_type: boolean\n' + 68 | ' default_value: true\n' + 69 | ' foreign_key_column: null\n' + 70 | ' foreign_key_table: null\n' + 71 | ' generation_expression: null\n' + 72 | ' has_auto_increment: false\n' + 73 | ' is_generated: false\n' + 74 | ' is_nullable: true\n' + 75 | ' is_primary_key: false\n' + 76 | ' is_unique: false\n' + 77 | ' max_length: null\n' + 78 | ' name: active\n' + 79 | ' numeric_precision: null\n' + 80 | ' numeric_scale: null\n' + 81 | ' table: auto_translation_settings\n' + 82 | ' type: boolean\n' + 83 | ' - collection: auto_translation_settings\n' + 84 | ' field: auth_key\n' + 85 | ' meta:\n' + 86 | ' collection: auto_translation_settings\n' + 87 | ' conditions: null\n' + 88 | ' display: null\n' + 89 | ' display_options: null\n' + 90 | ' field: auth_key\n' + 91 | ' group: null\n' + 92 | ' hidden: false\n' + 93 | ' interface: input\n' + 94 | ' note: >-\n' + 95 | ' Authentication - You need an authentication key to access to the API.\n' + 96 | ' https://www.deepl.com/de/your-account/keys\n' + 97 | ' options:\n' + 98 | ' iconLeft: key\n' + 99 | ' masked: true\n' + 100 | ' readonly: false\n' + 101 | ' required: false\n' + 102 | ' sort: 4\n' + 103 | ' special: null\n' + 104 | ' translations: null\n' + 105 | ' validation: null\n' + 106 | ' validation_message: null\n' + 107 | ' width: full\n' + 108 | ' schema:\n' + 109 | ' data_type: varchar\n' + 110 | ' default_value: null\n' + 111 | ' foreign_key_column: null\n' + 112 | ' foreign_key_table: null\n' + 113 | ' generation_expression: null\n' + 114 | ' has_auto_increment: false\n' + 115 | ' is_generated: false\n' + 116 | ' is_nullable: true\n' + 117 | ' is_primary_key: false\n' + 118 | ' is_unique: false\n' + 119 | ' max_length: 255\n' + 120 | ' name: auth_key\n' + 121 | ' numeric_precision: null\n' + 122 | ' numeric_scale: null\n' + 123 | ' table: auto_translation_settings\n' + 124 | ' type: string\n' + 125 | ' - collection: auto_translation_settings\n' + 126 | ' field: extra\n' + 127 | ' meta:\n' + 128 | ' collection: auto_translation_settings\n' + 129 | ' conditions: null\n' + 130 | ' display: null\n' + 131 | ' display_options: null\n' + 132 | ' field: extra\n' + 133 | ' group: visible_for_valid_auth_key\n' + 134 | ' hidden: false\n' + 135 | ' interface: input-multiline\n' + 136 | ' note: Informations about errors will be shown here.\n' + 137 | ' options: null\n' + 138 | ' readonly: true\n' + 139 | ' required: false\n' + 140 | ' sort: 4\n' + 141 | ' special: null\n' + 142 | ' translations: null\n' + 143 | ' validation: null\n' + 144 | ' validation_message: null\n' + 145 | ' width: full\n' + 146 | ' schema:\n' + 147 | ' data_type: text\n' + 148 | ' default_value: null\n' + 149 | ' foreign_key_column: null\n' + 150 | ' foreign_key_table: null\n' + 151 | ' generation_expression: null\n' + 152 | ' has_auto_increment: false\n' + 153 | ' is_generated: false\n' + 154 | ' is_nullable: true\n' + 155 | ' is_primary_key: false\n' + 156 | ' is_unique: false\n' + 157 | ' max_length: null\n' + 158 | ' name: extra\n' + 159 | ' numeric_precision: null\n' + 160 | ' numeric_scale: null\n' + 161 | ' table: auto_translation_settings\n' + 162 | ' type: text\n' + 163 | ' - collection: auto_translation_settings\n' + 164 | ' field: id\n' + 165 | ' meta:\n' + 166 | ' collection: auto_translation_settings\n' + 167 | ' conditions: null\n' + 168 | ' display: null\n' + 169 | ' display_options: null\n' + 170 | ' field: id\n' + 171 | ' group: null\n' + 172 | ' hidden: true\n' + 173 | ' interface: input\n' + 174 | ' note: null\n' + 175 | ' options: null\n' + 176 | ' readonly: true\n' + 177 | ' required: false\n' + 178 | ' sort: 1\n' + 179 | ' special: null\n' + 180 | ' translations: null\n' + 181 | ' validation: null\n' + 182 | ' validation_message: null\n' + 183 | ' width: full\n' + 184 | ' schema:\n' + 185 | ' data_type: integer\n' + 186 | ' default_value: null\n' + 187 | ' foreign_key_column: null\n' + 188 | ' foreign_key_table: null\n' + 189 | ' generation_expression: null\n' + 190 | ' has_auto_increment: true\n' + 191 | ' is_generated: false\n' + 192 | ' is_nullable: false\n' + 193 | ' is_primary_key: true\n' + 194 | ' is_unique: false\n' + 195 | ' max_length: null\n' + 196 | ' name: id\n' + 197 | ' numeric_precision: null\n' + 198 | ' numeric_scale: null\n' + 199 | ' table: auto_translation_settings\n' + 200 | ' type: integer\n' + 201 | ' - collection: auto_translation_settings\n' + 202 | ' field: informations\n' + 203 | ' meta:\n' + 204 | ' collection: auto_translation_settings\n' + 205 | ' conditions: null\n' + 206 | ' display: null\n' + 207 | ' display_options: null\n' + 208 | ' field: informations\n' + 209 | ' group: null\n' + 210 | ' hidden: false\n' + 211 | ' interface: input-multiline\n' + 212 | ' note: Informations about errors will be shown here.\n' + 213 | ' options: null\n' + 214 | ' readonly: true\n' + 215 | ' required: false\n' + 216 | ' sort: 3\n' + 217 | ' special: null\n' + 218 | ' translations: null\n' + 219 | ' validation: null\n' + 220 | ' validation_message: null\n' + 221 | ' width: full\n' + 222 | ' schema:\n' + 223 | ' data_type: text\n' + 224 | ' default_value: null\n' + 225 | ' foreign_key_column: null\n' + 226 | ' foreign_key_table: null\n' + 227 | ' generation_expression: null\n' + 228 | ' has_auto_increment: false\n' + 229 | ' is_generated: false\n' + 230 | ' is_nullable: true\n' + 231 | ' is_primary_key: false\n' + 232 | ' is_unique: false\n' + 233 | ' max_length: null\n' + 234 | ' name: informations\n' + 235 | ' numeric_precision: null\n' + 236 | ' numeric_scale: null\n' + 237 | ' table: auto_translation_settings\n' + 238 | ' type: text\n' + 239 | ' - collection: auto_translation_settings\n' + 240 | ' field: limit\n' + 241 | ' meta:\n' + 242 | ' collection: auto_translation_settings\n' + 243 | ' conditions: null\n' + 244 | ' display: null\n' + 245 | ' display_options: null\n' + 246 | ' field: limit\n' + 247 | ' group: usage\n' + 248 | ' hidden: false\n' + 249 | ' interface: input\n' + 250 | ' note: null\n' + 251 | ' options: null\n' + 252 | ' readonly: true\n' + 253 | ' required: false\n' + 254 | ' sort: 3\n' + 255 | ' special: null\n' + 256 | ' translations: null\n' + 257 | ' validation: null\n' + 258 | ' validation_message: null\n' + 259 | ' width: half\n' + 260 | ' schema:\n' + 261 | ' data_type: integer\n' + 262 | ' default_value: 500000\n' + 263 | ' foreign_key_column: null\n' + 264 | ' foreign_key_table: null\n' + 265 | ' generation_expression: null\n' + 266 | ' has_auto_increment: false\n' + 267 | ' is_generated: false\n' + 268 | ' is_nullable: true\n' + 269 | ' is_primary_key: false\n' + 270 | ' is_unique: false\n' + 271 | ' max_length: null\n' + 272 | ' name: limit\n' + 273 | ' numeric_precision: null\n' + 274 | ' numeric_scale: null\n' + 275 | ' table: auto_translation_settings\n' + 276 | ' type: integer\n' + 277 | ' - collection: auto_translation_settings\n' + 278 | ' field: notice\n' + 279 | ' meta:\n' + 280 | ' collection: auto_translation_settings\n' + 281 | ' conditions: null\n' + 282 | ' display: null\n' + 283 | ' display_options: null\n' + 284 | ' field: notice\n' + 285 | ' group: visible_for_valid_auth_key\n' + 286 | ' hidden: false\n' + 287 | ' interface: presentation-notice\n' + 288 | ' note: null\n' + 289 | ' options:\n' + 290 | ' text: >-\n' + 291 | ' If you want a collection (e. G. wikis) to be translated do the\n' + 292 | ' following. Add a field type "translations" which will create a new\n' + 293 | ' collection (e. G. wikis_translations). In this collection add the\n' + 294 | ' following boolean (default: true) fields:\n' + 295 | ' "be_source_for_translations", "let_be_translated" and\n' + 296 | ' "create_translations_for_all_languages". Ensure that Directus\n' + 297 | ' automatically created a collection "languages".\n' + 298 | ' readonly: false\n' + 299 | ' required: false\n' + 300 | ' sort: 1\n' + 301 | ' special:\n' + 302 | ' - alias\n' + 303 | ' - no-data\n' + 304 | ' translations: null\n' + 305 | ' validation: null\n' + 306 | ' validation_message: null\n' + 307 | ' width: full\n' + 308 | ' schema: null\n' + 309 | ' type: alias\n' + 310 | ' - collection: auto_translation_settings\n' + 311 | ' field: percentage\n' + 312 | ' meta:\n' + 313 | ' collection: auto_translation_settings\n' + 314 | ' conditions: null\n' + 315 | ' display: formatted-value\n' + 316 | ' display_options:\n' + 317 | ' suffix: \' %\'\n' + 318 | ' field: percentage\n' + 319 | ' group: usage\n' + 320 | ' hidden: false\n' + 321 | ' interface: slider\n' + 322 | ' note: null\n' + 323 | ' options:\n' + 324 | ' alwaysShowValue: true\n' + 325 | ' maxValue: 100\n' + 326 | ' minValue: 0\n' + 327 | ' readonly: true\n' + 328 | ' required: false\n' + 329 | ' sort: 1\n' + 330 | ' special: null\n' + 331 | ' translations: null\n' + 332 | ' validation: null\n' + 333 | ' validation_message: null\n' + 334 | ' width: full\n' + 335 | ' schema:\n' + 336 | ' data_type: integer\n' + 337 | ' default_value: null\n' + 338 | ' foreign_key_column: null\n' + 339 | ' foreign_key_table: null\n' + 340 | ' generation_expression: null\n' + 341 | ' has_auto_increment: false\n' + 342 | ' is_generated: false\n' + 343 | ' is_nullable: true\n' + 344 | ' is_primary_key: false\n' + 345 | ' is_unique: false\n' + 346 | ' max_length: null\n' + 347 | ' name: percentage\n' + 348 | ' numeric_precision: null\n' + 349 | ' numeric_scale: null\n' + 350 | ' table: auto_translation_settings\n' + 351 | ' type: integer\n' + 352 | ' - collection: auto_translation_settings\n' + 353 | ' field: usage\n' + 354 | ' meta:\n' + 355 | ' collection: auto_translation_settings\n' + 356 | ' conditions: null\n' + 357 | ' display: null\n' + 358 | ' display_options: null\n' + 359 | ' field: usage\n' + 360 | ' group: visible_for_valid_auth_key\n' + 361 | ' hidden: false\n' + 362 | ' interface: group-raw\n' + 363 | ' note: null\n' + 364 | ' options: null\n' + 365 | ' readonly: false\n' + 366 | ' required: false\n' + 367 | ' sort: 3\n' + 368 | ' special:\n' + 369 | ' - alias\n' + 370 | ' - group\n' + 371 | ' - no-data\n' + 372 | ' translations: null\n' + 373 | ' validation: null\n' + 374 | ' validation_message: null\n' + 375 | ' width: full\n' + 376 | ' schema: null\n' + 377 | ' type: alias\n' + 378 | ' - collection: auto_translation_settings\n' + 379 | ' field: used\n' + 380 | ' meta:\n' + 381 | ' collection: auto_translation_settings\n' + 382 | ' conditions: null\n' + 383 | ' display: raw\n' + 384 | ' display_options: null\n' + 385 | ' field: used\n' + 386 | ' group: usage\n' + 387 | ' hidden: false\n' + 388 | ' interface: input\n' + 389 | ' note: null\n' + 390 | ' options: null\n' + 391 | ' readonly: false\n' + 392 | ' required: false\n' + 393 | ' sort: 2\n' + 394 | ' special: null\n' + 395 | ' translations: null\n' + 396 | ' validation: null\n' + 397 | ' validation_message: null\n' + 398 | ' width: half\n' + 399 | ' schema:\n' + 400 | ' data_type: integer\n' + 401 | ' default_value: 0\n' + 402 | ' foreign_key_column: null\n' + 403 | ' foreign_key_table: null\n' + 404 | ' generation_expression: null\n' + 405 | ' has_auto_increment: false\n' + 406 | ' is_generated: false\n' + 407 | ' is_nullable: true\n' + 408 | ' is_primary_key: false\n' + 409 | ' is_unique: false\n' + 410 | ' max_length: null\n' + 411 | ' name: used\n' + 412 | ' numeric_precision: null\n' + 413 | ' numeric_scale: null\n' + 414 | ' table: auto_translation_settings\n' + 415 | ' type: integer\n' + 416 | ' - collection: auto_translation_settings\n' + 417 | ' field: valid_auth_key\n' + 418 | ' meta:\n' + 419 | ' collection: auto_translation_settings\n' + 420 | ' conditions: null\n' + 421 | ' display: null\n' + 422 | ' display_options: null\n' + 423 | ' field: valid_auth_key\n' + 424 | ' group: null\n' + 425 | ' hidden: true\n' + 426 | ' interface: boolean\n' + 427 | ' note: null\n' + 428 | ' options: null\n' + 429 | ' readonly: true\n' + 430 | ' required: false\n' + 431 | ' sort: 2\n' + 432 | ' special:\n' + 433 | ' - cast-boolean\n' + 434 | ' translations: null\n' + 435 | ' validation: null\n' + 436 | ' validation_message: null\n' + 437 | ' width: full\n' + 438 | ' schema:\n' + 439 | ' data_type: boolean\n' + 440 | ' default_value: false\n' + 441 | ' foreign_key_column: null\n' + 442 | ' foreign_key_table: null\n' + 443 | ' generation_expression: null\n' + 444 | ' has_auto_increment: false\n' + 445 | ' is_generated: false\n' + 446 | ' is_nullable: true\n' + 447 | ' is_primary_key: false\n' + 448 | ' is_unique: false\n' + 449 | ' max_length: null\n' + 450 | ' name: valid_auth_key\n' + 451 | ' numeric_precision: null\n' + 452 | ' numeric_scale: null\n' + 453 | ' table: auto_translation_settings\n' + 454 | ' type: boolean\n' + 455 | ' - collection: auto_translation_settings\n' + 456 | ' field: visible_for_valid_auth_key\n' + 457 | ' meta:\n' + 458 | ' collection: auto_translation_settings\n' + 459 | ' conditions:\n' + 460 | ' - rule:\n' + 461 | ' _and:\n' + 462 | ' - valid_auth_key:\n' + 463 | ' _eq: false\n' + 464 | ' readonly: true\n' + 465 | ' hidden: true\n' + 466 | ' options: {}\n' + 467 | ' display: null\n' + 468 | ' display_options: null\n' + 469 | ' field: visible_for_valid_auth_key\n' + 470 | ' group: null\n' + 471 | ' hidden: false\n' + 472 | ' interface: group-raw\n' + 473 | ' note: null\n' + 474 | ' options: null\n' + 475 | ' readonly: false\n' + 476 | ' required: false\n' + 477 | ' sort: 5\n' + 478 | ' special:\n' + 479 | ' - alias\n' + 480 | ' - group\n' + 481 | ' - no-data\n' + 482 | ' translations: null\n' + 483 | ' validation: null\n' + 484 | ' validation_message: null\n' + 485 | ' width: full\n' + 486 | ' schema: null\n' + 487 | ' type: alias\n' + 488 | 'relations: []\n' 489 | }; 490 | -------------------------------------------------------------------------------- /dev/extensions/directus-extension-auto-translation/src/DirectusCollectionTranslator.ts: -------------------------------------------------------------------------------- 1 | export class DirectusCollectionTranslator { 2 | static FIELD_BE_SOURCE_FOR_TRANSLATION = "be_source_for_translations"; 3 | static FIELD_LET_BE_TRANSLATED = "let_be_translated"; 4 | 5 | static FIELD_LANGUAGES_IDS_NEW = "languages_id" 6 | static FIELD_LANGUAGES_CODE_OLD = "languages_code" 7 | static FIELD_LANGUAGES_ID_OR_CODE = undefined; 8 | 9 | static COLLECTION_LANGUAGES = "languages"; 10 | 11 | /** 12 | * We only need to translate if there are translations to translate 13 | * Therefore check if there are new translations to create 14 | * or if there are translations to update 15 | */ 16 | static areTranslationsToTranslate(payload: any) { 17 | if (!!payload && !!payload.translations) { 18 | let newTranslationsActions = payload?.translations || {}; 19 | let newTranslationsCreateActions = newTranslationsActions?.create || []; 20 | let newTranslationsUpdateActions = newTranslationsActions?.update || []; 21 | return newTranslationsCreateActions.length > 0 || newTranslationsUpdateActions.length > 0; 22 | } 23 | return false; 24 | } 25 | 26 | static getSourceTranslationFromTranslations(translations: any, schema: any, collectionName: any) { 27 | if (!!translations && translations.length > 0) { 28 | for (let translation of translations) { 29 | let let_be_source_for_translation = DirectusCollectionTranslator.getValueFromPayloadOrDefaultValue(translation, DirectusCollectionTranslator.FIELD_BE_SOURCE_FOR_TRANSLATION, schema, collectionName); 30 | if (!!let_be_source_for_translation) { 31 | return translation; 32 | } 33 | } 34 | } 35 | } 36 | 37 | static getSourceTranslationFromListsOfTranslations(listsOfTranslations, schema, collectionName) { 38 | if (!!listsOfTranslations && listsOfTranslations.length > 0) { 39 | for (let i = 0; i < listsOfTranslations.length; i++) { 40 | let translations = listsOfTranslations[i]; 41 | let sourceTranslation = DirectusCollectionTranslator.getSourceTranslationFromTranslations(translations, schema, collectionName); 42 | if (!!sourceTranslation) { 43 | return sourceTranslation; 44 | } 45 | } 46 | } 47 | return null; 48 | } 49 | 50 | /** 51 | * This is due to a change from languages_code to languages_ids newer than directus 9.20.1 (or mayer much newer like 10) 52 | * therefore we identify which field is used and set it accordingly 53 | * @param translation 54 | */ 55 | static setFIELD_LANGUAGES_ID_OR_CODE(translation){ 56 | const translationFieldOld = translation?.[DirectusCollectionTranslator.FIELD_LANGUAGES_CODE_OLD] 57 | if(!!translationFieldOld){ 58 | DirectusCollectionTranslator.FIELD_LANGUAGES_ID_OR_CODE = DirectusCollectionTranslator.FIELD_LANGUAGES_CODE_OLD 59 | } 60 | const translationFieldNew = translation?.[DirectusCollectionTranslator.FIELD_LANGUAGES_IDS_NEW] 61 | if(!!translationFieldNew){ 62 | DirectusCollectionTranslator.FIELD_LANGUAGES_ID_OR_CODE = DirectusCollectionTranslator.FIELD_LANGUAGES_IDS_NEW 63 | } 64 | } 65 | 66 | static parseTranslationListToLanguagesCodeDict(translations) { 67 | let languagesCodeDict = {}; 68 | for (let translation of translations) { 69 | DirectusCollectionTranslator.setFIELD_LANGUAGES_ID_OR_CODE(translation); 70 | 71 | languagesCodeDict[translation?.[DirectusCollectionTranslator.FIELD_LANGUAGES_ID_OR_CODE]?.code] = translation; 72 | } 73 | return languagesCodeDict; 74 | } 75 | 76 | static async modifyPayloadForTranslation(currentItem, payload, translator, translatorSettings, itemsServiceCreator, schema, collectionName, translations_field) { 77 | if (DirectusCollectionTranslator.areTranslationsToTranslate(payload)) { 78 | let workPayload = JSON.parse(JSON.stringify(payload)); 79 | 80 | /** 81 | workPayload 82 | { 83 | "translations": { 84 | "create": [], 85 | "update": [ 86 | { 87 | "description": "Okay was geht ab?", 88 | "languages_id": { 89 | "code": "de-DE" 90 | }, 91 | "id": 1 92 | } 93 | ], 94 | "delete": [] 95 | } 96 | } 97 | */ 98 | 99 | let currentTranslations = currentItem?.[translations_field] || []; //need to know, if we need to update old translations or create them 100 | 101 | /** 102 | currentTranslations 103 | [ 104 | { 105 | "id": 1, 106 | "test_id": 1, 107 | "languages_id": "de-DE", 108 | "be_source_for_translation": true, 109 | "let_be_translated": true, 110 | "create_translations_for_all_languages": true, 111 | "description": "Okay was geht ab?" 112 | }, 113 | { 114 | "id": 2, 115 | "test_id": 1, 116 | "languages_id": "ar-SA", 117 | "be_source_for_translation": false, 118 | "let_be_translated": true, 119 | "create_translations_for_all_languages": true, 120 | "description": null 121 | } 122 | ] 123 | */ 124 | 125 | let existingTranslations = {}; 126 | for (let translation of currentTranslations) { 127 | existingTranslations[translation?.[DirectusCollectionTranslator.FIELD_LANGUAGES_ID_OR_CODE]] = translation; 128 | } 129 | 130 | let newTranslationsActions = workPayload?.[translations_field] || {}; 131 | let newTranslationsCreateActions = newTranslationsActions?.create || []; 132 | let newTranslationsUpdateActions = newTranslationsActions?.update || []; 133 | 134 | let newTranslationsCreateLanguageDict = DirectusCollectionTranslator.parseTranslationListToLanguagesCodeDict(newTranslationsCreateActions); 135 | let newTranslationsUpdateLanguageDict = DirectusCollectionTranslator.parseTranslationListToLanguagesCodeDict(newTranslationsUpdateActions); 136 | 137 | let sourceTranslationInExistingItem = DirectusCollectionTranslator.getSourceTranslationFromListsOfTranslations([currentTranslations], schema, collectionName); 138 | let sourceTranslationInPayload = DirectusCollectionTranslator.getSourceTranslationFromListsOfTranslations([newTranslationsCreateActions, newTranslationsUpdateActions], schema, collectionName); 139 | 140 | let sourceTranslation = sourceTranslationInPayload || sourceTranslationInExistingItem 141 | //TODO Maybe throw an error if multiple source translations are found? 142 | 143 | if (sourceTranslation) { // we should always have a source translation, since we checked if there are update or create translations 144 | let sourceTranslationLanguageCode = sourceTranslation?.[DirectusCollectionTranslator.FIELD_LANGUAGES_ID_OR_CODE]?.code; 145 | //console.log("sourceTranslationLanguageCode: ", sourceTranslationLanguageCode); 146 | 147 | let languagesService = itemsServiceCreator.getItemsService(DirectusCollectionTranslator.COLLECTION_LANGUAGES); 148 | let languages = await languagesService.readByQuery({}); 149 | if (languages.length > 0) { 150 | let translationsToCreate = []; 151 | let translationsToUpdate = []; 152 | let translationsToDelete = []; 153 | 154 | let fieldsToTranslate = DirectusCollectionTranslator.getFieldsToTranslate(schema, collectionName); 155 | 156 | for (let language of languages) { 157 | let language_code = language?.code; 158 | //console.log("--------"); 159 | //console.log("Check for language_code: ", language_code); 160 | 161 | let existingTranslation = existingTranslations[language_code]; 162 | let isSourceTranslation = language_code === sourceTranslationLanguageCode; 163 | 164 | if (!!existingTranslation) { // we have an existing translation, so we need to update it 165 | /** 166 | * UPDATE 167 | */ 168 | //console.log("There is an existingTranslation"); 169 | if (isSourceTranslation) { 170 | //console.log("Its the source translation, we just pass it through"); 171 | //TODO set be_source_for_translation to false 172 | translationsToUpdate.push({ 173 | ...sourceTranslation, 174 | }); 175 | } else { 176 | //console.log("Its not the source translation, we need to check if it needs to be updated"); 177 | let translationInPayload = newTranslationsUpdateLanguageDict[language_code]; 178 | 179 | //check if in the payload the user has given the field "let_be_translated" and overwrite the existing value if it exists 180 | let letBeTranslatedInExistingTranslation = existingTranslation?.[DirectusCollectionTranslator.FIELD_LET_BE_TRANSLATED]; 181 | //console.log("The existing translation has the field let_be_translated: ", letBeTranslatedInExistingTranslation); 182 | let createTranslation = letBeTranslatedInExistingTranslation; 183 | let letBeTranslatedInPayload = DirectusCollectionTranslator.getValueFromPayloadOrDefaultValue(translationInPayload, DirectusCollectionTranslator.FIELD_LET_BE_TRANSLATED, schema, collectionName); 184 | //console.log("The translation in the payload has the field let_be_translated: ", letBeTranslatedInPayload); 185 | if (DirectusCollectionTranslator.isValueDefined(letBeTranslatedInPayload)) { //if payload has false or true, overwrite existing value 186 | createTranslation = letBeTranslatedInPayload; 187 | } 188 | //console.log("The translation in the payload will be created: ", createTranslation); 189 | 190 | if (!!createTranslation) { 191 | //console.log("Create translation"); 192 | let translatedItem = await DirectusCollectionTranslator.translateTranslationItem(sourceTranslation, language_code, translator, translatorSettings, fieldsToTranslate); 193 | translationsToUpdate.push({ 194 | ...existingTranslation, 195 | ...translatedItem}); 196 | } else if (!!translationInPayload) { //The user has given a payload but dont want it to be translated 197 | //console.log("Use the given payload") 198 | translationsToUpdate.push({ 199 | ...translationInPayload, 200 | [DirectusCollectionTranslator.FIELD_BE_SOURCE_FOR_TRANSLATION]: false, //but we dont want it to be the source translation anymore 201 | }); 202 | } else { 203 | //console.log("No payload given for this language"); 204 | } 205 | } 206 | } else { 207 | /** 208 | * CREATE 209 | */ 210 | //console.log("No existingTranslation"); 211 | if (isSourceTranslation) { 212 | //TODO set be_source_for_translation to false 213 | //console.log("Its the source translation, we just pass it through"); 214 | translationsToCreate.push({ 215 | ...sourceTranslation, 216 | [DirectusCollectionTranslator.FIELD_LET_BE_TRANSLATED]: DirectusCollectionTranslator.getValueFromPayloadOrDefaultValue(sourceTranslation, DirectusCollectionTranslator.FIELD_LET_BE_TRANSLATED, schema, collectionName), 217 | [DirectusCollectionTranslator.FIELD_BE_SOURCE_FOR_TRANSLATION]: true, 218 | }); 219 | } else { 220 | //console.log("Its not the source translation, we need to check if it needs to be created"); 221 | //If we dont have an existing translation and the permission to all languages is set 222 | let translationInPayload = newTranslationsCreateLanguageDict[language_code]; 223 | 224 | //console.log("translationInPayload: "); 225 | //console.log(translationInPayload); 226 | let letBeTranslatedInPayload = DirectusCollectionTranslator.getValueFromPayloadOrDefaultValue(translationInPayload, DirectusCollectionTranslator.FIELD_LET_BE_TRANSLATED, schema, collectionName); 227 | let letBeTranslated = true; //only if the user explicitly set it to false, we dont create the translation, otherwise on undefined we create it 228 | //console.log("letBeTranslatedInPayload", letBeTranslatedInPayload); 229 | if (DirectusCollectionTranslator.isValueDefined(letBeTranslatedInPayload)) { //if payload has false or true, overwrite existing value 230 | //console.log("letBeTranslatedInPayload is defined"); 231 | letBeTranslated = letBeTranslatedInPayload; 232 | } 233 | 234 | if (letBeTranslated) { 235 | //console.log("Create translation"); 236 | let translatedItem = await DirectusCollectionTranslator.translateTranslationItem(sourceTranslation, language?.code, translator, translatorSettings, fieldsToTranslate); 237 | translationsToCreate.push({ 238 | ...translatedItem 239 | }) 240 | } else if (!!translationInPayload) { //The user has given a payload but dont want it to be translated 241 | //console.log("Use the given payload") 242 | translationsToCreate.push({ 243 | ...translationInPayload, 244 | [DirectusCollectionTranslator.FIELD_BE_SOURCE_FOR_TRANSLATION]: false, //but we dont want it to be the source translation 245 | }); 246 | } else { 247 | //console.log("No payload given for this language"); 248 | } 249 | } 250 | } 251 | } 252 | 253 | payload[translations_field] = { 254 | create: translationsToCreate, 255 | update: translationsToUpdate, 256 | delete: translationsToDelete 257 | }; 258 | return payload; //We musst alter the payload reference ! 259 | } 260 | } 261 | } 262 | return payload; //return does not matter 263 | } 264 | 265 | static isValueDefined(value) { 266 | return value !== undefined && value !== null; 267 | } 268 | 269 | static getValueFromPayloadOrDefaultValue(payloadItem, fieldName, schema, collectionName) { 270 | let translationCollectionSchema = DirectusCollectionTranslator.getTranslationCollectionSchema(schema, collectionName); 271 | 272 | let valueInPayload = payloadItem?.[fieldName]; 273 | if (DirectusCollectionTranslator.isValueDefined(valueInPayload)) { //if payload has false or true, overwrite existing value 274 | return valueInPayload; 275 | } else { //nothing found? use the default value 276 | let defaultValue = translationCollectionSchema?.fields?.[fieldName]?.defaultValue; 277 | return defaultValue; 278 | } 279 | } 280 | 281 | static async translateTranslationItem(sourceTranslation, language_code, translator, translatorSettings, fieldsToTranslate) { 282 | let translatedItem = {}; 283 | if (!!fieldsToTranslate && fieldsToTranslate.length > 0) { 284 | for (let field of fieldsToTranslate) { 285 | let fieldValue = sourceTranslation[field]; 286 | if (!!fieldValue) { 287 | try { 288 | let translatedValue = await translator.translate(fieldValue, sourceTranslation?.[DirectusCollectionTranslator.FIELD_LANGUAGES_ID_OR_CODE]?.code, language_code); 289 | if (!!translatedValue) { 290 | translatedItem[field] = translatedValue; 291 | } else { 292 | //TODO: check if this would ever happen 293 | } 294 | } catch (err) { 295 | //TODO: error handling? 296 | console.log(err); 297 | } 298 | } 299 | } 300 | } 301 | 302 | translatedItem[DirectusCollectionTranslator.FIELD_LANGUAGES_ID_OR_CODE] = { 303 | "code": language_code 304 | } 305 | translatedItem[DirectusCollectionTranslator.FIELD_LET_BE_TRANSLATED] = true; //if we create a translation, we want it in the future also 306 | translatedItem[DirectusCollectionTranslator.FIELD_BE_SOURCE_FOR_TRANSLATION] = false; //if translated it wont be the source translation anymore 307 | return translatedItem; 308 | } 309 | 310 | static getTranslationCollectionName(collectionName) { 311 | return collectionName + "_translations"; 312 | } 313 | 314 | 315 | static getTranslationCollectionSchema(schema, collectionName) { 316 | let translationCollectionName = DirectusCollectionTranslator.getTranslationCollectionName(collectionName); 317 | let collectionInformations = schema?.collections?.[translationCollectionName]; //special case for translations relation 318 | /** 319 | { 320 | ... 321 | fields: { 322 | ... 323 | let_be_translated: { 324 | field: 'let_be_translated', 325 | defaultValue: true, 326 | ... 327 | } 328 | } 329 | } 330 | */ 331 | return collectionInformations; 332 | } 333 | 334 | /** 335 | * Gets a list of all fields that are translatable 336 | * Only watches for text and string 337 | * Ignores the primary key field 338 | * Ignores fields that are relations 339 | */ 340 | static getFieldsToTranslate(schema, collectionName) { 341 | let translationCollectionName = DirectusCollectionTranslator.getTranslationCollectionName(collectionName); 342 | let collectionInformations = DirectusCollectionTranslator.getTranslationCollectionSchema(schema, collectionName); 343 | let collectionFieldsInformationsDict = collectionInformations?.fields || {}; 344 | let collectionFields = Object.keys(collectionFieldsInformationsDict); 345 | 346 | let primaryFieldKey = collectionInformations?.primary || "id"; //we need to know the primary field key 347 | 348 | let fieldsToTranslateDict = {}; 349 | for (let field of collectionFields) { 350 | if (field !== primaryFieldKey) { //we dont translate the primary field 351 | let fieldsInformation = collectionFieldsInformationsDict[field]; 352 | /**" 353 | "content": { 354 | "type": "text", 355 | ... 356 | }, 357 | }, 358 | "title": { 359 | "type": "string", 360 | ... 361 | */ 362 | //we only translate fields of type string and text 363 | //TODO: check if there are more field types 364 | if (fieldsInformation?.type === "text" || fieldsInformation?.type === "string") { 365 | fieldsToTranslateDict[field] = true; 366 | } 367 | } 368 | } 369 | 370 | //We should now remove all relations fields 371 | let relations = schema?.relations || []; 372 | let translationRelations = []; 373 | for (let relation of relations) { 374 | /** 375 | { 376 | "collection": "wikis_translations", 377 | "field": "wikis_id", 378 | "related_collection": "wikis", 379 | ... 380 | } 381 | */ 382 | if (relation?.collection === translationCollectionName) { 383 | delete fieldsToTranslateDict[relation?.field]; //we dont translate the relation field 384 | } 385 | } 386 | 387 | let fieldsToTranslate = Object.keys(fieldsToTranslateDict); 388 | return fieldsToTranslate; 389 | } 390 | } 391 | --------------------------------------------------------------------------------