├── .gitignore ├── .DS_Store ├── screenshot-v2.jpg ├── composer.json ├── index.css ├── index.php ├── readme.md ├── src └── translationStatus.php └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doldenroller/kirby-translation-status/HEAD/.DS_Store -------------------------------------------------------------------------------- /screenshot-v2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doldenroller/kirby-translation-status/HEAD/screenshot-v2.jpg -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doldenroller/kirby-translation-status", 3 | "description": "Show all available translations of a page and if they are finished.", 4 | "type": "kirby-plugin", 5 | "authors": [ 6 | { 7 | "name": "Doldenroller", 8 | "email": "tom@doldenroller.de" 9 | } 10 | ], 11 | "keywords": [ 12 | "kirby5", 13 | "kirby5-cms", 14 | "kirby5-plugin", 15 | "kirby5-multilanguage" 16 | ], 17 | "require": { 18 | "getkirby/composer-installer": "^1.1" 19 | }, 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | .k-box:not([data-theme="none"]) {border-left: 2px solid var(--color-gray-500); border-radius: 0;} 3 | .k-box:not([data-theme="none"]):first-of-type {border-top-left-radius: var(--rounded); border-top-right-radius: var(--rounded);} 4 | .k-box:not([data-theme="none"]):last-of-type {border-bottom-left-radius: var(--rounded); border-bottom-right-radius: var(--rounded);} 5 | 6 | .k-box[data-theme="translation-head"] {background: var(--color-gray-400);} 7 | .k-box[data-theme="translated"] {background: var(--color-gray-100); border-left-color: var(--color-green-400) !important;} 8 | .k-box[data-theme="not-translated"] {background: var(--color-gray-100); border-left-color: var(--color-orange-400) !important;} 9 | 10 | .k-box[data-theme="translated"] + .k-box[data-theme="not-translated"] {border-top: 2px solid var(--color-gray-400);} 11 | 12 | .k-text ol, .k-text ul {padding-inline-start: 1em;} 13 | .k-box .k-button {padding-inline-start: 0;} 14 | 15 | .k-translated-section .k-text {width: 100%;} 16 | .k-translation-buttons .k-button-group {justify-content: space-between;} 17 | .k-translation-buttons .k-button[aria-current] {text-decoration: underline;} 18 | 19 | .k-info-icon {display: inline-flex; vertical-align: text-bottom;} 20 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'translationstatus' => require __DIR__ . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'translationStatus.php' 9 | ], 10 | 11 | // delete route 12 | 'api' => [ 13 | 'routes' => function (\Kirby\Cms\App $kirby) { 14 | return [ 15 | // route to delete content file 16 | [ 17 | 'pattern' => 'plugin-translationstatus/delete', 18 | 'method' => 'POST', 19 | 'action' => function () use ($kirby) { 20 | 21 | //$id = str_replace('pages/', '', get('id')); 22 | //$id = str_replace('+', '/', $id); 23 | 24 | $id = str_replace('+', '/', substr(get('id'), strlen('pages/'))); 25 | $languageCode = get('languageCode'); 26 | 27 | // Protect default lang 28 | if ($kirby->defaultLanguage()->code() === $languageCode) { 29 | return [ 30 | 'code' => 403, 31 | 'text' => t('translations.delete.false', 'The default language content can not be deleted.'), 32 | ]; 33 | }elseif($page = $kirby->page($id)){ 34 | 35 | if($page->translation($languageCode)->exists()){ 36 | try{ 37 | $page->version('latest')->delete($languageCode); 38 | return [ 39 | 'code' => 200, 40 | 'text' => tt('translations.delete.success', null, ['code' => $languageCode]), 41 | 'default' => $kirby->defaultLanguage()->code(), // not the prettiest, but I have no clue how to integrate this in th js-part 42 | ]; 43 | }catch (Exception $error){ 44 | return [ 45 | 'code' => 500, 46 | 'text' => tt('translations.delete.error', null, ['code' => $languageCode]), 47 | ]; 48 | } 49 | 50 | // file exists 51 | }else{ 52 | return [ 53 | 'code' => 200, 54 | 'text' => tt('translations.page.notranslation', null, ['code' => $languageCode]), 55 | 'default' => $kirby->defaultLanguage()->code(), // not the prettiest, but I have no clue how to integrate this in th js-part 56 | ]; 57 | } 58 | // page exists 59 | }else{ 60 | return [ 61 | 'code' => 404, 62 | 'text' => tt('translations.page.notfound', null, ['page' => $id]), 63 | ]; 64 | } 65 | 66 | } 67 | ], // end route to delete content file 68 | 69 | ]; 70 | } 71 | ], 72 | // delete route 73 | 74 | 'translations' => [ 75 | 'en' => [ 76 | 'translations.delete.confirm' => 'Do you really want to delete the content of this language?', 77 | 'translations.delete.success' => 'The language ({{code}}) has been successfully deleted.', 78 | 'translations.delete.error' => 'The language ({{code}}) could not be deleted.', 79 | 'translations.delete.false' => 'The default language can not be deleted.', 80 | 'translations.page.notranslation' => 'The language ({{code}}) was not translated.', 81 | 'translations.page.notfound' => 'The page {{page}} doesn\'t exist.', 82 | 'translations.language.switch' => 'Switch to {{language}}', 83 | 'translations.finished' => 'Finished', 84 | 'translations.unfinished' => 'To be done', 85 | 'translations.all' => 'All Translations done', 86 | ], 87 | 'de' => [ 88 | 'translations.delete.confirm' => 'Möchten Sie den Inhalt dieser Sprache wirklich löschen?', 89 | 'translations.delete.success' => 'Die Sprache ({{code}}) wurde erfolgreich gelöscht.', 90 | 'translations.delete.error' => 'Die Sprache ({{code}}) konnte nicht gelöscht werden.', 91 | 'translations.delete.false' => 'Die Standardsprache kann nicht gelöscht werden.', 92 | 'translations.page.notranslation' => 'Die Sprache ({{code}}) ist nicht übersetzt.', 93 | 'translations.page.notfound' => 'Die Seite {{page}} existiert nicht.', 94 | 'translations.language.switch' => 'Zu {{language}} wechseln', 95 | 'translations.finished' => 'Übersetzt', 96 | 'translations.unfinished' => 'Zu erledigen', 97 | 'translations.all' => 'Alle Übersetzungen angelegt', 98 | ], 99 | 100 | ], 101 | 102 | ]); 103 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Kirby Translation Status 2 | 3 | Infosection to display all translations of a page in two lists, seperated wether the language is translated or not. By default a message is displayed if all languages are translated. Additionally the the template of the current page is displayed. 4 | ![screenshot translation-status](/screenshot-v2.jpg) 5 | 6 | ## Install 7 | ### Download Zip file 8 | 9 | Copy plugin folder into `site/plugins` 10 | 11 | ### Composer 12 | I have no clue about the composer... but if I get right this should work. 13 | Run `composer require doldenroller/kirby-translation-status`. 14 | 15 | ## Usage 16 | Find and show translations of your page in the panel or use it as language switch in your blueprints. 17 | 18 | ### Example 19 | Basic setup: 20 | 21 | ```yaml 22 | sections: 23 | mysection: 24 | headline: Page Translations 25 | type: translationstatus 26 | finished: Translated Languages 27 | unfinished: Translations to be done 28 | allfinished: All Translations done 29 | ``` 30 | 31 | 32 | With translations in Blueprint: 33 | 34 | ```yaml 35 | sections: 36 | mysection: 37 | headline: Page Translations 38 | type: translationstatus 39 | finished: 40 | en: Translated Languages 41 | de: Übersetzte Sprachen 42 | unfinished: 43 | en: Translations to be done 44 | de: Noch zu erledigen 45 | allfinished: 46 | en: All Translations done 47 | de: Alle Übersetzungen angelegt 48 | extend: true 49 | ``` 50 | 51 | Translations can also be setup in your languagefiles of your setup: 52 | 53 | ```yaml 54 | 'translations.finished' => '', 55 | 'translations.unfinished' => '', 56 | 'translations.all' => '', 57 | 58 | ``` 59 | 60 | More Options: 61 | With the `extend: true` property, the finished list is still showing when all translations are done, so you can still use it as a language switch. 62 | The `template: false` property hides the intended Template information. 63 | 64 | And finally you can exclude pages by template. Either with the `ignore:` property like this: 65 | 66 | ```yaml 67 | # as comma-sperated list 68 | ignore: news, jobs 69 | 70 | # or as normal list 71 | ignore: 72 | - news 73 | - jobs 74 | 75 | ``` 76 | 77 | This can also be setup as global option in your config 78 | 79 | ```php 80 | 81 | // again either comma-separeted 82 | 'doldenroller.templatestatus.ignore' => 'solutions, default' 83 | 84 | // or as array 85 | 'doldenroller.templatestatus.ignore' => ['solutions', 'default'] 86 | 87 | ``` 88 | 89 | ### Update/Changes in v2.0 90 | First of all Kirby4 support is added. And the active language is underlined. 91 | 92 | The config option changed from `templatestatus.ignore` to `doldenroller.templatestatus.ignore`. 93 | 94 | And now it is possible to delete a translated content file. This can also be disabled or restricted in the blueprint or in config. The restriction can be made that only user-roles or users can delete the translated content. By now the users are found by their e-mailadresses. Maybe UUID support will be added in the future. 95 | 96 | In the blueprint restrictions can be set with the `delete:` property like this: 97 | 98 | ```yaml 99 | # complete disabled 100 | delete: false 101 | 102 | # as comma-sperated list 103 | delete: admin, chief-editor@website.com 104 | 105 | # or as normal list 106 | delete: 107 | - admin 108 | - chief-editor@website.com 109 | 110 | ``` 111 | 112 | Or as global config-option: 113 | 114 | ```php 115 | // complete disabled 116 | 'doldenroller.templatestatus.delete' => false 117 | 118 | // again either comma-separeted 119 | 'doldenroller.templatestatus.delete' => 'admin, chief-editor@website.com' 120 | 121 | // or as array 122 | 'doldenroller.templatestatus.delete' => ['admin', 'chief-editor@website.com'] 123 | 124 | ``` 125 | 126 | In the examples above all users with the`admin` user role and the user with the e-mailadress `chief-editor@website.com` can delete translated content. 127 | 128 | ### Update/Changes in v3.0 129 | Kirby 5 support is added. 130 | 131 | ## Possible enhencements 132 | These could be difficult but would be nice features: 133 | 1. Update hint, that shows when content is updated 134 | 2. ~~Delete / reset language, because sometimes its easier to start from scratch~~ 135 | 3. Add user identification by user-UUID (Not my prioraty, because I mostly work without UUIDs) 136 | 4. ~~Refresh/update section when new translation is created~~ 137 | 5. ~~Check compability with Kirby5~~ 138 | 139 | ## Older Versions 140 | - For Kirby 3 please use [v1.0.0](https://github.com/doldenroller/kirby-translation-status/releases/tag/v1.0.0) 141 | - For Kirby 4 please use [v2.0.0](https://github.com/doldenroller/kirby-translation-status/releases/tag/v2.0.0) 142 | 143 | ## License 144 | 145 | [MIT](https://opensource.org/licenses/MIT) 146 | 147 | 148 | It is discouraged to use this plugin in any project that promotes racism, sexism, homophobia animal abuse, violence or any other form of hate speech. 149 | -------------------------------------------------------------------------------- /src/translationStatus.php: -------------------------------------------------------------------------------- 1 | user()->language(); 13 | if(is_array($text) && !empty($text[$panellang])){ 14 | $text = $text[$panellang]; 15 | }elseif(is_array($text) && empty($text[$panellang])){ 16 | $text = reset($text); // first value 17 | }elseif(empty($text) && !empty($fallback)){ 18 | $text = $fallback; 19 | } 20 | return $text; 21 | }; 22 | 23 | 24 | $extension = [ 25 | 'props' => [ 26 | 'headline' => function ($headline = null) { 27 | if(is_array($headline)){ 28 | $headline = textValue($headline); 29 | } 30 | return $headline; 31 | }, 32 | 'label' => function ($label = null) { 33 | if(is_array($label)){ 34 | $label = textValue($label); 35 | } 36 | return $label; 37 | }, 38 | 39 | 'extend' => function (bool $extend = null) { 40 | return $extend; 41 | }, 42 | 'delete' => function ($deleteable = null) { 43 | return $deleteable; 44 | } 45 | 46 | ], 47 | 48 | 49 | 'computed' => [ 50 | 'translated' => function () { 51 | 52 | // declare basics 53 | $kirby = kirby(); 54 | $panellang = $kirby->user()->language(); 55 | $languages = $kirby->languages(); 56 | $page = $this->model(); 57 | $userrole = $kirby->user()->role()->id(); 58 | $useremail = $kirby->user()->email(); 59 | 60 | // on default translations can be deleted 61 | $deleteable = true; 62 | 63 | // check for restrictions to delete a translation (blueprint beats config) 64 | $delete = $this->delete() ?? $kirby->option('doldenroller.translations.delete'); 65 | if($delete !== null || (is_bool($delete) && $delete === false)){ 66 | 67 | // completly disabled 68 | if(is_bool($delete) && $delete === false){ 69 | $deleteable = false; 70 | }elseif(!empty($delete)){ 71 | if(!is_array($delete)) $delete = Str::split($delete); 72 | // if(!in_array($useremail, $delete) || !in_array($userrole, $delete)) - doesnt work somehow... so we wrap it. Not as beautiful, but it works 73 | if(!(in_array($useremail, $delete) || in_array($userrole, $delete)) ){ 74 | $deleteable = false; 75 | } 76 | } 77 | 78 | } 79 | 80 | 81 | // create arrays and fill them 82 | $done = $empty = null; 83 | foreach($languages as $lang){ 84 | $code = $lang->code(); 85 | //$contentFile = $page->storage()->translation($code)->contentFile(); 86 | $title = tt('translations.language.switch', null, ['language' => $lang->name()]); 87 | 88 | // check for default language 89 | $notdefault = true; 90 | if($lang->isDefault()){ 91 | $notdefault = false; 92 | } 93 | 94 | if($page->translation($code)->exists()){ 95 | $done[] = ['code' => $code, 'name' => $lang->name(), 'deleteable' => $deleteable, 'notdefault' => $notdefault, 'title' => $title]; 96 | }else{ 97 | $empty[] = ['code' => $code, 'name' => $lang->name(), 'title' => $title]; 98 | } 99 | }; 100 | 101 | // create headlines 102 | $finHead = textValue($this->finished(), t('translations.finished', 'Finished')); 103 | $unHead = textValue($this->unfinished(), t('translations.unfinished', 'To be done')); 104 | 105 | if( (!is_bool($this->extend()) && $this->extend() !== false) || (is_bool($this->extend()) && $this->extend() === false) ){ 106 | if(!empty($done) && empty($empty)){ 107 | $finHead = textValue($this->allfinished(), t('translations.all', 'All Finished')); 108 | $unHead = null; 109 | } 110 | } 111 | 112 | //$template = $page->blueprint()->title(); 113 | $template = [ 114 | 'name' => $page->blueprint()->title(), 115 | 'icon' => $page->blueprint()->icon() 116 | ]; 117 | if(is_bool($this->template()) && $this->template() === false){ 118 | $template = null; 119 | } 120 | 121 | // ignore templates 122 | if($ignore = $this->ignore() ?? $kirby->option('doldenroller.templatestatus.ignore')){ 123 | if(!is_array($ignore)) $ignore = Str::split($ignore); 124 | if(in_array($page->intendedTemplate(), $ignore)){ 125 | $done = $empty = $finHead = $unHead = null; 126 | } 127 | 128 | } 129 | 130 | // return the data 131 | $status = []; 132 | $status['template'] = $template; 133 | $status['finished'] = $done; 134 | $status['unfinished'] = $empty; 135 | $status['finHead'] = $finHead; 136 | $status['unHead'] = $unHead; 137 | //$status['deleteTest'] = $delete; 138 | 139 | return $status; 140 | } 141 | 142 | ], 143 | 144 | ]; 145 | return $extension; 146 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | panel.plugin('doldenroller/kirby-translation-status', { 2 | sections: { 3 | translationstatus: { 4 | data: function () { 5 | return { 6 | label: null, 7 | headline: null, 8 | //text: null, 9 | extend: null, 10 | deletable: null, 11 | defaultLang: null, 12 | translated: Array 13 | } 14 | }, 15 | 16 | methods: { 17 | change(language) { 18 | this.$emit("change", language); 19 | this.$go(window.location, { 20 | query: { 21 | language: language 22 | } 23 | }); 24 | }, 25 | 26 | // delete method 27 | deleteTranslationOpen(language, code, text) { 28 | this.dialogLanguage = code; 29 | let dialogText = text + ' (' + language + ')'; 30 | this.$panel.dialog.open({ 31 | component: 'k-remove-dialog', 32 | props: { 33 | text: dialogText, 34 | }, 35 | on: { 36 | submit: () => { 37 | this.$api.post('plugin-translationstatus/delete', {id: window.panel.view.path, languageCode: code}) 38 | .then(response => { 39 | if (response.code === 200) { 40 | window.panel.notification.success(response.text); 41 | //console.log(window.panel.view); 42 | this.change(response.default); 43 | // not the finest solution, but at least no error. there should be a way to updated only the section but i don't know how... 44 | window.setTimeout(() => { 45 | location.reload(); 46 | }, 300); 47 | 48 | } 49 | else { 50 | window.panel.notification.error(response.text); 51 | } 52 | }) 53 | .catch(error => { 54 | console.log(error); 55 | window.panel.notification.error(error); 56 | }); 57 | } // submit 58 | } 59 | }) // dialog 60 | }, 61 | // delete method 62 | 63 | }, 64 | 65 | created: function() { 66 | this.load().then(response => { 67 | this.label = response.label; 68 | this.headline = response.headline; 69 | //this.text = response.text; 70 | this.translated = response.translated; 71 | 72 | window.panel.events.on('model.update', () => { 73 | if(!window.panel.language.isDefault){ 74 | if(this.translated.unfinished !== null && this.translated.unfinished.findIndex((x) => x.code === window.panel.language.code) !== -1 ){ 75 | console.log('SAVE', this.translated.unfinished); 76 | window.setTimeout(() => { 77 | location.reload(); 78 | }, 300); 79 | } 80 | } 81 | }); 82 | 83 | 84 | 85 | }); 86 | }, 87 | 88 | template: ` 89 |
90 | 91 |
92 | {{ label }} 93 |
94 | 95 |
96 | {{ headline }} 97 |
98 | 99 | 100 | 101 |

Template: {{ translated.template.name }}

102 |
103 |
104 | 105 | 106 | 107 | {{ translated.finHead }} 108 |
    109 |
  • 110 | 111 | 112 | 113 | 114 | 115 |
  • 116 |
117 |
118 |
119 | 120 | 121 | 122 | {{ translated.unHead }} 123 |
    124 |
  • 125 | 126 | 127 |
  • 128 |
129 |
130 |
131 | 132 | 133 |
134 | ` 135 | } 136 | 137 | } 138 | 139 | }); 140 | --------------------------------------------------------------------------------