├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── TranslatedLayout.gif ├── composer.json ├── index.css ├── index.js ├── index.php ├── kirbyup.config.ts ├── package.json └── src ├── blueprints └── fields │ └── translatedlayoutwithfieldsets.yml ├── classes ├── TranslatedBlockTraits.php ├── TranslatedBlocksField.php ├── TranslatedLayoutField.php ├── TranslatedLayoutFieldContent.php └── TranslatedLayoutHelpers.php ├── components ├── TranslatedBlock.vue ├── TranslatedBlocks.vue ├── TranslatedBlocksField.vue ├── TranslatedLayout.vue ├── TranslatedLayoutField.vue ├── TranslatedLayoutHelpers.js ├── TranslatedLayoutMixin.js └── TranslatedLayouts.vue └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.php] 13 | indent_size = 4 14 | 15 | [*.md,*.txt] 16 | trim_trailing_whitespace = false 17 | insert_final_newline = false 18 | 19 | [composer.json] 20 | indent_size = 4 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS files 2 | .DS_Store 3 | 4 | # npm modules 5 | /node_modules 6 | 7 | # Composer files 8 | /vendor 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Daan de Lange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kirby TranslatedLayout field plugin 2 | 3 | This plugin brings translation logics into the native `layouts` fields. 4 | 5 | ### Experimental 6 | 7 | While the kirby team is waiting for some heavy refactoring for recursively bringing translation logics into their "complex fields", this plugin aims to provide a temporary workaround for multi-language websites. 8 | 9 | This is an experimental draft trying to bring some translation logic to blocks, columns and layouts. 10 | It turns out to be quite powerful already with just a minimal set of changes compared to the native field behaviour. 11 | 12 | **Current state** : 13 | Tested on a single configuration, works well but not extensively tested. Therefore, please note that **there remains a risk of data loss**. (Do not use without backups!) 14 | There is no `translatedblocks` fields nor `toTranslatedBlocks` method (yet?), please use layouts instead. 15 | 16 | ### Implementation 17 | 18 | - The **primary language** (default) inherits the default `LayoutField` behaviour and remains *(almost?)* identical to the native Kirby Layout field. 19 | - The **seconday languages** (translations) of this field are always syncronized on parse (aka `$field->fill($value)`). 20 | - **Identical structures** : The layouts and blocks structures are defined by the default language using their unique `id`. 21 | - **Fallback** : If a block has no translation, it's replaced with the default language. 22 | - **Sanitation** : If a block translation is not available in the default language, it's removed. All blocks from the default language are guaranteed to be available for translation in the panel. 23 | - **Panel GUI** : Non-translateable fields and blocks are disabled, preventing panel users from changing the layout and adding blocks in translations. 24 | - **Data** : The syncronized translation is saved as a blocks and columns array and parsed on retrieval. (this saves some disk space and makes data more readable). 25 | 26 | ![Screenshot of Kirby 3 plugins TranslatedLayout](TranslatedLayout.gif) 27 | 28 | ## Requirements 29 | 30 | - Version `0.3.3-beta` : Kirby 3.8 or above. 31 | - *(Kirby 4 compatibility should be easy to implement; mainly a few changed function signatures and namespace renames)* 32 | - Version `1.0.0` : Kirby 5 or above. 33 | 34 | - **Note**: This plugin heavily relies on the use of the panel. If you'd like to manually edit a `translatedlayout` field via the text content file, it's not recommended to use this plugin, as it's probably not recommended to use blocks without the panel. (Meanwhile, it still is possible, and this plugin even simplifies the translation files). 35 | 36 | ## Installation 37 | 38 | _Choose one:_ 39 | 40 | - Download: Download and copy this repository to `/site/plugins/translatedlayout`. 41 | - Git submodule: `git submodule add https://github.com/daandelange/kirby-translatedlayout.git site/plugins/translatedlayout`. 42 | - Composer: `composer require daandelange/translatedlayout`. 43 | 44 | ## Setup 45 | 46 | ### Import existing data 47 | 48 | - The default language saves as the native Kirby layouts field. 49 | - Translations have a different content structure and only save the translated block fields. 50 | 51 | **Warning!** If you already have a layout with translated content, switching to this field will erase all translations unless you manually give the same `id` to blocks/rows/columns in the translations data structure. There is no automatic script available. 52 | The same happens when you change the default language so make sure it's correct, and to never change it again. 53 | 54 | 55 | ### Blueprints 56 | 57 | In your page blueprints, you can simply replace a `type: layout` field by `type: translatedlayout`. Read more about how to use the respective fields in the official Kirby docs. 58 | 59 | The only difference is an extra `translate` property on fields, please refer to this example: 60 | 61 | ````yml 62 | sections: 63 | content: 64 | type: fields 65 | fields: 66 | mylayout: 67 | label: Translated Layout Demo 68 | type: translatedlayout 69 | translate: true # <--- enables syncing of translations (layout field) 70 | layouts: 71 | - "1/1" 72 | - "1/2, 1/2" 73 | - "1/3, 1/3, 1/3" 74 | fieldsets: 75 | translateable: 76 | label: Fully Translateable Blocks 77 | type: group 78 | fieldsets: 79 | heading: 80 | extends: blocks/heading 81 | translate: true # same as default value 82 | - list 83 | - text 84 | partiallytranslateable: 85 | label: Blocks with some translateable fields 86 | type: group 87 | fieldsets: 88 | image: # over-rule the translated option of existing fields 89 | label: Image (non translateable src) 90 | type: image 91 | translate: false 92 | fields: 93 | link: 94 | translate: false 95 | url: # custom block example 96 | name: Url (non-translateable source) 97 | icon: cog 98 | fields: 99 | link: 100 | type: url 101 | translate: false 102 | required: true 103 | text: 104 | type: text 105 | translate: true 106 | 107 | nontranslateable: 108 | label: Non-translated blocks 109 | type: group 110 | fieldsets: 111 | line: 112 | extends: blocks/line 113 | translate: false # Completely disable whole block translations 114 | settings: # You can also translate layout settings 115 | fields: 116 | class: 117 | type: text 118 | width: 1/2 119 | translate: false # Don't translate 120 | purpose: 121 | type: text 122 | translate: true # Translate 123 | myblock: 124 | label: Translated Blocks Demo 125 | type: translatedblocks 126 | fieldsets: 127 | heading: 128 | extends: blocks/heading 129 | translate: true # same as default value 130 | - text 131 | line: 132 | extends: blocks/line 133 | translate: false # Completely disable whole block translations 134 | ```` 135 | 136 | To use predefined translation settings for the default kirby blocks, you may use : 137 | 138 | ````yml 139 | fields: 140 | content: 141 | type: translatedlayout 142 | extends: fields/translatedlayoutwithfieldsets 143 | ```` 144 | This can be useful for quickly setting up this plugin in a test environment. 145 | *Beware that this will add the fields to your fieldsets if they don't exist yet.* 146 | 147 | To setup your own fieldsets, prefer copy/pasting from [translatedlayoutwithfieldsets.yml](https://github.com/Daandelange/kirby-TranslatedLayout/blob/master/src/blueprints/fields/translatedlayoutwithfieldsets.yml) and adapt it to your needs. 148 | 149 | ### Templates 150 | 151 | Use `$field->toTranslatedLayout()` in your templates to fetch & render the field contents. Like the native `LayoutField`'s `toLayouts`, a `Kirby\Cms\Layouts` object is returned. There is absolutely no difference as the plugin acts during the data parse state. 152 | 153 | ## Options 154 | 155 | There are no options available yet. Would you like to contribute some ? 156 | 157 | ## Development 158 | 159 | - A small hack to fix KirbyUp's alias `@KirbyPanel` sub-includes : ([more info](https://github.com/johannschopplich/kirbyup/issues/7)) 160 | - osx: `cd /path/to/translatedLayout/ && ln -s "../../../../kirby/panel/src/mixins" ./src/mixins` 161 | - linux: *todo* 162 | - other: Create an alias/symlink pointing from `translatedlayout/src/mixins` to `/kirby/panel/src/mixins`. 163 | - `npm install` : Install the required dependencies. 164 | - `npm run dev` : Develop mode (listen/compile). 165 | - `npm run build` : Compile for publishing. 166 | 167 | ## Feature ideas 168 | 169 | - Plugin options : Set rather to fill with (untranslated) default language, or leave the translateable blocks empty ? (on translation creation only). 170 | - Write some test cases. 171 | 172 | ## Similar Plugins 173 | 174 | - [Synced-Structure](https://gist.github.com/lukaskleinschmidt/1c0b94ffab51d650b7c7605a4d25c213) : Syncs structures across languages using UUIDs. Note: _This method doesn't work with `Layouts` and `Blocks` fields because they use the `FieldClass` instead of Kirby's field blueprints._ 175 | 176 | ## License 177 | 178 | MIT - Free to use, free to improve ! 179 | 180 | However, for usage in commercial projects, please seriously consider to improve the plugin a little and contribute back the changes with a PR, or hire someone to do so. 181 | For contribution suggestions, you can search for `todo` in the source code or refer to open issues. 182 | 183 | ## Credits 184 | 185 | - [Daan de Lange](https://daandelange.com/) 186 | -------------------------------------------------------------------------------- /TranslatedLayout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daandelange/kirby-TranslatedLayout/469b13db8e05c2fdbb351bdeeb1e33a12786902f/TranslatedLayout.gif -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.5", 3 | "name": "daandelange/translatedlayout", 4 | "description": "Layout and blocks fields with embedded translation logic.", 5 | "type": "kirby-plugin", 6 | "license": "MIT", 7 | "homepage": "https://github.com/daandelange/kirby-translatedlayout", 8 | "author": "Daan de Lange", 9 | "authors": [ 10 | { 11 | "name": "Daan de Lange", 12 | "homepage": "https://daandelange.com/" 13 | } 14 | ], 15 | "keywords": [ 16 | "kirby5", 17 | "kirby3", 18 | "kirby-cms", 19 | "kirby-plugin", 20 | "kirby-field-plugin", 21 | "kirby-panel-plugin" 22 | ], 23 | "require": { 24 | "getkirby/composer-installer": "^1.2" 25 | }, 26 | "conflict": { 27 | "getkirby/cms": "<5.0.0-beta.6" 28 | }, 29 | "extra": { 30 | "installer-name": "translatedlayout" 31 | }, 32 | "support": { 33 | "docs": "https://github.com/daandelange/kirby-translatedlayout/blob/main/README.md", 34 | "source": "https://github.com/daandelange/kirby-translatedlayout" 35 | } 36 | } -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | .k-layout-column{position:relative;height:100%;display:flex;flex-direction:column;min-height:6rem}.k-layout-column:focus{outline:0}.k-layout-column>.k-blocks{box-shadow:none;padding:0;height:100%;background:light-dark(var(--color-white),var(--color-gray-850));min-height:4rem}.k-layout-column>.k-blocks[data-empty=true]{min-height:6rem}.k-layout-column>.k-blocks>.k-blocks-list{display:flex;flex-direction:column;height:100%}.k-layout-column>.k-blocks>.k-blocks-list>.k-block-container:last-of-type{flex-grow:1}.k-layout-column>.k-blocks>.k-blocks-list+.k-blocks-empty.k-box{--box-color-back: transparent;position:absolute;top:0;right:0;bottom:0;left:0;justify-content:center;opacity:0;transition:opacity .3s;border:0}.k-layout-column>.k-blocks>.k-blocks-list+.k-blocks-empty:hover{opacity:1}.k-layout{--layout-border-color: var(--color-gray-300);--layout-toolbar-width: 2rem;position:relative;padding-right:var(--layout-toolbar-width);box-shadow:var(--shadow)}[data-disabled=true] .k-layout{padding-right:0}.k-layout:not(:last-of-type){margin-bottom:var(--spacing-2)}.k-layout:focus{outline:0}.k-layout-toolbar{position:absolute;top:0;bottom:0;right:0;width:var(--layout-toolbar-width);display:flex;flex-direction:column;align-items:center;justify-content:space-between;padding-bottom:var(--spacing-2);font-size:var(--text-sm);background:light-dark(var(--color-gray-100),var(--color-gray-850));border-left:1px solid var(--panel-color-back);color:var(--color-gray-500);border-radius:var(--rounded)}.k-layout-toolbar:hover{color:light-dark(var(--color-black),var(--color-white))}.k-layout-toolbar-button{width:var(--layout-toolbar-width);height:var(--layout-toolbar-width)}.k-layout-columns.k-grid{grid-gap:1px;background:var(--panel-color-back)}.k-layout:not(:first-child) .k-layout-columns.k-grid{border-top:0}.k-layouts .k-sortable-ghost{position:relative;box-shadow:#11111140 0 5px 10px;outline:2px solid var(--color-focus);cursor:grabbing;z-index:1}.k-translated-blocks-field.blocks-disabled .k-blocks button.k-blocks-empty{cursor:default} 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function(){"use strict";const ut="";function u(s,t,n,e,i,o,h,c){var l=typeof s=="function"?s.options:s;t&&(l.render=t,l.staticRenderFns=n,l._compiled=!0),e&&(l.functional=!0),o&&(l._scopeId="data-v-"+o);var a;if(h?(a=function(r){r=r||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,!r&&typeof __VUE_SSR_CONTEXT__<"u"&&(r=__VUE_SSR_CONTEXT__),i&&i.call(this,r),r&&r._registeredComponents&&r._registeredComponents.add(h)},l._ssrRegister=a):i&&(a=c?function(){i.call(this,(l.functional?this.parent:this).$root.$options.shadowRoot)}:i),a)if(l.functional){l._injectStyles=a;var d=l.render;l.render=function(rt,y){return a.call(y),d(rt,y)}}else{var p=l.beforeCreate;l.beforeCreate=p?[].concat(p,a):[a]}return{exports:s,options:l}}const _={props:{endpoints:Object,fieldsetGroups:Object,fieldsets:Object,id:String,isSelected:Boolean}},k={mixins:[_],props:{blocks:Array,width:{type:String,default:"1/1"}},emits:["input"]};var b=function(){var t=this,n=t._self._c;return n("div",{staticClass:"k-column k-layout-column",style:{"--width":t.width},attrs:{id:t.id,tabindex:"0"},on:{dblclick:function(e){return t.$refs.blocks.choose(t.blocks.length)}}},[n("k-blocks",t._b({ref:"blocks",on:{input:function(e){return t.$emit("input",e)}},nativeOn:{dblclick:function(e){e.stopPropagation()}}},"k-blocks",{endpoints:t.endpoints,fieldsets:t.fieldsets,fieldsetGroups:t.fieldsetGroups,group:"layout",value:t.blocks},!1))],1)},v=[],g=u(k,b,v,!1,null,null,null,null);const $=g.exports,C={props:{disabled:Boolean}},w={props:{id:{type:[Number,String],default(){return this._uid}}}},dt="",m={mixins:[_,C],props:{columns:Array,layouts:{type:Array,default:()=>[["1/1"]]},settings:Object}},x={mixins:[m],props:{attrs:[Array,Object]},emits:["append","change","copy","duplicate","prepend","remove","select","updateAttrs","updateColumn"],computed:{options(){return[{click:()=>this.$emit("prepend"),icon:"angle-up",text:this.$t("insert.before")},{click:()=>this.$emit("append"),icon:"angle-down",text:this.$t("insert.after")},"-",{click:()=>this.openSettings(),icon:"settings",text:this.$t("settings"),when:this.$helper.object.isEmpty(this.settings)===!1},{click:()=>this.$emit("duplicate"),icon:"copy",text:this.$t("duplicate")},{click:()=>this.$emit("change"),disabled:this.layouts.length===1,icon:"dashboard",text:this.$t("field.layout.change")},"-",{click:()=>this.$emit("copy"),icon:"template",text:this.$t("copy")},{click:()=>this.$emit("paste"),icon:"download",text:this.$t("paste.after")},"-",{click:()=>this.remove(),icon:"trash",text:this.$t("field.layout.delete")}]},tabs(){let s=this.settings.tabs;for(const[t,n]of Object.entries(s))for(const e in n.fields)s[t].fields[e].endpoints={field:this.endpoints.field+"/fields/"+e,section:this.endpoints.section,model:this.endpoints.model};return s}},methods:{openSettings(){this.$panel.drawer.open({component:"k-form-drawer",props:{icon:"settings",tabs:this.tabs,title:this.$t("settings"),value:this.attrs},on:{input:s=>this.$emit("updateAttrs",s)}})},remove(){this.$panel.dialog.open({component:"k-remove-dialog",props:{text:this.$t("field.layout.delete.confirm")},on:{submit:()=>{this.$emit("remove"),this.$panel.dialog.close()}}})}}};var I=function(){var t=this,n=t._self._c;return n("section",{staticClass:"k-layout",attrs:{"data-selected":t.isSelected,tabindex:"0"},on:{click:function(e){return t.$emit("select")}}},[n("k-grid",{staticClass:"k-layout-columns"},t._l(t.columns,function(e,i){return n("k-layout-column",t._b({key:e.id,on:{input:function(o){return t.$emit("updateColumn",{column:e,columnIndex:i,blocks:o})}}},"k-layout-column",{...e,endpoints:t.endpoints,fieldsetGroups:t.fieldsetGroups,fieldsets:t.fieldsets},!1))}),1),t.disabled?t._e():n("nav",{staticClass:"k-layout-toolbar"},[t.settings?n("k-button",{staticClass:"k-layout-toolbar-button",attrs:{title:t.$t("settings"),icon:"settings"},on:{click:t.openSettings}}):t._e(),n("k-button",{staticClass:"k-layout-toolbar-button",attrs:{icon:"angle-down"},on:{click:function(e){return t.$refs.options.toggle()}}}),n("k-dropdown-content",{ref:"options",attrs:{options:t.options,"align-x":"end"}}),n("k-sort-handle")],1)],1)},D=[],E=u(x,I,D,!1,null,null,null,null);const N=E.exports,f={computed:{layoutEditingIsDisabled(){return this.$panel.language?!this.$panel.language.default&&this.isWithinTranslatedComponent:!1},isWithinTranslatedComponent(){let s=this;const t=["translatedblocks","translatedlayout"];for(;s!=this.$root&&s!=null&&s!=null;){if(s.type&&t.includes(s.type))return!0;s=s.$parent}return!1}},methods:{invertCustomAndNativeFunctions(s){for(const t of s){{if(!this[t]){window.console.log("Native function replacement hack: `"+t+"` doesn't exist anymore. Please fix me.");continue}if(!this[t+"Custom"]){window.console.log("Native function replacement hack: `"+t+"Custom` doesn't exist. Please implement it !");continue}}this[t+"Native"]||(this[t+"Native"]=this[t],this[t]=this[t+"Custom"])}}}},ct="",A={extends:N,components:{"k-layout-column":$},mixins:[f]};var F=function(){var t=this,n=t._self._c;return n("section",{staticClass:"k-layout",attrs:{"data-selected":t.isSelected,tabindex:"0"},on:{click:function(e){return t.$emit("select")}}},[n("k-grid",{staticClass:"k-layout-columns"},t._l(t.columns,function(e,i){return n("k-layout-column",t._b({key:e.id,on:{input:function(o){return t.$emit("updateColumn",{column:e,columnIndex:i,blocks:o})}}},"k-layout-column",{...e,endpoints:t.endpoints,fieldsetGroups:t.fieldsetGroups,fieldsets:t.fieldsets},!1))}),1),t.disabled?t._e():n("nav",{staticClass:"k-layout-toolbar"},[t.settings?n("k-button",{staticClass:"k-layout-toolbar-button",attrs:{title:t.$t("settings"),icon:"settings"},on:{click:t.openSettings}}):t._e(),t.layoutEditingIsDisabled?t._e():n("k-button",{staticClass:"k-layout-toolbar-button",attrs:{icon:"angle-down"},on:{click:function(e){return t.$refs.options.toggle()}}}),n("k-dropdown-content",{ref:"options",attrs:{options:t.options,"align-x":"end"}}),t.layoutEditingIsDisabled?t._e():n("k-sort-handle")],1)],1)},S=[],T=u(A,F,S,!1,null,null,null,null);const O=T.exports,pt="",K={mixins:[{mixins:[m,w],props:{empty:String,min:Number,max:Number,selector:Object,value:{type:Array,default:()=>[]}}}],emits:["input"],data(){return{current:null,nextIndex:null,rows:this.value,selected:null}},computed:{draggableOptions(){return{handle:!0,list:this.rows}},hasFieldsets(){return this.$helper.object.length(this.fieldsets)>0}},watch:{value(){this.rows=this.value}},methods:{copy(s,t){if(this.rows.length===0)return!1;const n=t!==void 0?this.rows[t]:this.rows;this.$helper.clipboard.write(JSON.stringify(n),s),this.$panel.notification.success({message:this.$t("copy.success.multiple",{count:n.length??1}),icon:"template"})},change(s,t){const n=t.columns.map(i=>i.width),e=this.layouts.findIndex(i=>i.toString()===n.toString());this.$panel.dialog.open({component:"k-layout-selector",props:{label:this.$t("field.layout.change"),layouts:this.layouts,selector:this.selector,value:this.layouts[e]},on:{submit:i=>{this.onChange(i,e,{rowIndex:s,layoutIndex:e,layout:t}),this.$panel.dialog.close()}}})},duplicate(s,t){const n=this.$helper.object.clone(t),e=this.updateIds(n);this.rows.splice(s+1,0,...e),this.save()},async onAdd(s){let t=await this.$api.post(this.endpoints.field+"/layout",{columns:s});this.rows.splice(this.nextIndex,0,t),this.save()},async onChange(s,t,n){if(t===this.layouts[n.layoutIndex])return;const e=n.layout,i=await this.$api.post(this.endpoints.field+"/layout",{attrs:e.attrs,columns:s}),o=e.columns.filter(c=>{var l;return((l=c==null?void 0:c.blocks)==null?void 0:l.length)>0}),h=[];if(o.length===0)h.push(i);else{const c=Math.ceil(o.length/i.columns.length)*i.columns.length;for(let l=0;l{var r;return d.blocks=((r=o[p+l])==null?void 0:r.blocks)??[],d}),a.columns.filter(d=>{var p;return(p=d==null?void 0:d.blocks)==null?void 0:p.length}).length&&h.push(a)}}this.rows.splice(n.rowIndex,1,...h),this.save()},async paste(s,t=this.rows.length){let n=await this.$api.post(this.endpoints.field+"/layout/paste",{json:this.$helper.clipboard.read(s)});n.length&&(this.rows.splice(t,0,...n),this.save()),this.$panel.notification.success({message:this.$t("paste.success",{count:n.length}),icon:"download"})},pasteboard(s){this.$panel.dialog.open({component:"k-block-pasteboard",on:{paste:t=>this.paste(t,s)}})},remove(s){const t=this.rows.findIndex(n=>n.id===s.id);t!==-1&&this.$delete(this.rows,t),this.save()},removeAll(){this.$panel.dialog.open({component:"k-remove-dialog",props:{text:this.$t("field.layout.delete.confirm.all")},on:{submit:()=>{this.rows=[],this.save(),this.$panel.dialog.close()}}})},save(){this.$emit("input",this.rows)},select(s){if(this.nextIndex=s,this.layouts.length===1)return this.onAdd(this.layouts[0]);this.$panel.dialog.open({component:"k-layout-selector",props:{layouts:this.layouts,selector:this.selector,value:null},on:{submit:t=>{this.onAdd(t),this.$panel.dialog.close()}}})},updateAttrs(s,t){this.rows[s].attrs=t,this.save()},updateColumn(s){this.rows[s.index].columns[s.columnIndex].blocks=s.blocks,this.save()},updateIds(s){return Array.isArray(s)===!1&&(s=[s]),s.map(t=>(t.id=this.$helper.uuid(),t.columns=t.columns.map(n=>(n.id=this.$helper.uuid(),n.blocks=n.blocks.map(e=>(e.id=this.$helper.uuid(),e)),n)),t))}}};var P=function(){var t=this,n=t._self._c;return n("div",[t.hasFieldsets&&t.rows.length?[n("k-draggable",t._b({staticClass:"k-layouts",on:{sort:t.save}},"k-draggable",t.draggableOptions,!1),t._l(t.rows,function(e,i){return n("k-layout",t._b({key:e.id,on:{append:function(o){return t.select(i+1)},change:function(o){return t.change(i,e)},copy:function(o){return t.copy(o,i)},duplicate:function(o){return t.duplicate(i,e)},paste:function(o){return t.pasteboard(i+1)},prepend:function(o){return t.select(i)},remove:function(o){return t.remove(e)},select:function(o){t.selected=e.id},updateAttrs:function(o){return t.updateAttrs(i,o)},updateColumn:function(o){return t.updateColumn({layout:e,index:i,...o})}}},"k-layout",{...e,disabled:t.disabled,endpoints:t.endpoints,fieldsetGroups:t.fieldsetGroups,fieldsets:t.fieldsets,isSelected:t.selected===e.id,layouts:t.layouts,settings:t.settings},!1))}),1)]:t.hasFieldsets===!1?n("k-empty",{staticClass:"k-layout-empty",attrs:{icon:"dashboard"}},[t._v(" "+t._s(t.$t("field.blocks.fieldsets.empty"))+" ")]):n("k-empty",{staticClass:"k-layout-empty",attrs:{icon:"dashboard"},on:{click:function(e){return t.select(0)}}},[t._v(" "+t._s(t.empty??t.$t("field.layout.empty"))+" ")])],2)},R=[],L=u(K,P,R,!1,null,null,null,null);const G=L.exports,ht="",j={extends:G,components:{"k-translated-layout":O},data(){return{current:null,nextIndex:null,rows:this.value,selected:null}},mixins:[f],mounted:function(){this.invertCustomAndNativeFunctions(["onAdd","remove","select","change","copy","duplicate"])},methods:{async onAddCustom(s){return this.layoutEditingIsDisabled?null:this.onAddNative(s)},duplicateCustom(s,t){return this.layoutEditingIsDisabled?null:this.duplicateNative(s,t)},removeCustom(s){return this.layoutEditingIsDisabled?null:this.removeNative(s)},selectCustom(s){return this.layoutEditingIsDisabled?null:this.selectNative(s)},changeCustom(s,t){return this.layoutEditingIsDisabled?null:this.changeNative(s,t)},copyCustom(s,t){return this.layoutEditingIsDisabled?null:this.copyNative(s,t)}}};var B=function(){var t=this,n=t._self._c;return n("div",[t.hasFieldsets&&t.rows.length?[n("k-draggable",t._b({staticClass:"k-layouts k-translated-layouts",on:{sort:t.save}},"k-draggable",t.draggableOptions,!1),t._l(t.rows,function(e,i){return n("k-translated-layout",t._b({key:e.id,on:{append:function(o){return t.select(i+1)},change:function(o){return t.change(i,e)},copy:function(o){return t.copy(o,i)},duplicate:function(o){return t.duplicate(i,e)},paste:function(o){return t.pasteboard(i+1)},prepend:function(o){return t.select(i)},remove:function(o){return t.remove(e)},select:function(o){t.selected=e.id},updateAttrs:function(o){return t.updateAttrs(i,o)},updateColumn:function(o){return t.updateColumn({layout:e,index:i,...o})}}},"k-translated-layout",{...e,disabled:t.disabled,endpoints:t.endpoints,fieldsetGroups:t.fieldsetGroups,fieldsets:t.fieldsets,isSelected:t.selected===e.id,layouts:t.layouts,settings:t.settings},!1))}),1)]:t.hasFieldsets===!1?n("k-empty",{staticClass:"k-layout-empty k-translated-layout-empty",attrs:{icon:"dashboard"}},[t._v(" "+t._s(t.$t("field.blocks.fieldsets.empty"))+" ")]):t.layoutEditingIsDisabled?n("k-empty",{staticClass:"k-layout-empty k-translated-layout-empty",attrs:{icon:"dashboard"},on:{click:function(e){return t.select(0)}}},[t._v(" "+t._s(t.empty??t.$t("field.layout.empty"))+" ")]):t._e()],2)},U=[],W=u(j,B,U,!1,null,null,null,null);const M=W.exports,_t="",z={extends:"k-layout-field",components:{"k-translated-layouts":M},mixins:[f]};var H=function(){var t=this,n=t._self._c;return n("k-field",t._b({class:{"k-translated-layout-field":!0,"k-layout-field":!0,"layouts-disabled":t.layoutEditingIsDisabled},style:t.$attrs.style,scopedSlots:t._u([!t.disabled&&t.hasFieldsets?{key:"options",fn:function(){return[t.layoutEditingIsDisabled?t._e():n("k-button-group",{attrs:{layout:"collapsed"}},[n("k-button",{staticClass:"input-focus",attrs:{autofocus:t.autofocus,text:t.$t("add"),icon:"add",variant:"filled",size:"xs"},on:{click:function(e){return t.$refs.layouts.select(0)}}}),n("k-button",{attrs:{icon:"dots",variant:"filled",size:"xs"},on:{click:function(e){return t.$refs.options.toggle()}}}),n("k-dropdown-content",{ref:"options",attrs:{options:t.options,"align-x":"end"}})],1)]},proxy:!0}:null],null,!0)},"k-field",t.$props,!1),[n("k-input-validator",t._b({attrs:{value:JSON.stringify(t.value)}},"k-input-validator",{min:t.min,max:t.max,required:t.required},!1),[n("k-translated-layouts",t._b({ref:"layouts",on:{input:function(e){return t.$emit("input",e)}}},"k-translated-layouts",t.$props,!1))],1),!t.disabled&&t.hasFieldsets&&!t.layoutEditingIsDisabled?n("footer",[n("k-button",{attrs:{title:t.$t("add"),icon:"add",size:"xs",variant:"filled"},on:{click:function(e){return t.$refs.layouts.select(t.value.length)}}})],1):t._e()],1)},q=[],J=u(z,H,q,!1,null,null,null,null);const V=J.exports,mt="",X={extends:"k-blocks-field",components:{},mixins:[f]};var Q=function(){var t=this,n=t._self._c;return n("k-field",t._b({class:{"k-blocks-field":!0,"k-translated-blocks-field":!0,"blocks-disabled":t.layoutEditingIsDisabled},scopedSlots:t._u([{key:"options",fn:function(){return[t.hasFieldsets&&!t.layoutEditingIsDisabled?n("k-dropdown",[n("k-button",{attrs:{icon:"dots"},on:{click:function(e){return t.$refs.options.toggle()}}}),n("k-dropdown-content",{ref:"options",attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"add"},on:{click:function(e){return t.$refs.blocks.choose(t.value.length)}}},[t._v(" "+t._s(t.$t("add"))+" ")]),n("hr"),n("k-dropdown-item",{attrs:{disabled:t.isEmpty,icon:"template"},on:{click:function(e){return t.$refs.blocks.copyAll()}}},[t._v(" "+t._s(t.$t("copy.all"))+" ")]),n("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"download"},on:{click:function(e){return t.$refs.blocks.pasteboard()}}},[t._v(" "+t._s(t.$t("paste"))+" ")]),n("hr"),n("k-dropdown-item",{attrs:{disabled:t.isEmpty,icon:"trash"},on:{click:function(e){return t.$refs.blocks.confirmToRemoveAll()}}},[t._v(" "+t._s(t.$t("delete.all"))+" ")])],1)],1):t._e()]},proxy:!0}])},"k-field",t.$props,!1),[t.layoutEditingIsDisabled&&t.isEmpty?n("k-empty",{staticClass:"k-blocks-empty",attrs:{icon:"box"}},[t._v(" "+t._s(t.empty||t.$t("field.blocks.empty"))+" ")]):n("k-blocks",t._g({ref:"blocks",attrs:{autofocus:t.autofocus,compact:!1,empty:t.empty,endpoints:t.endpoints,fieldsets:t.fieldsets,"fieldset-groups":t.fieldsetGroups,group:t.group,max:t.max,value:t.value},on:{close:function(e){t.opened=e},open:function(e){t.opened=e}}},t.$listeners)),!t.isEmpty&&!t.isFull&&!t.layoutEditingIsDisabled?n("k-button",{staticClass:"k-field-add-item-button",attrs:{icon:"add",tooltip:t.$t("add")},on:{click:function(e){return t.$refs.blocks.choose(t.value.length)}}}):t._e()],1)},Y=[],Z=u(X,Q,Y,!1,null,null,null,null);const tt=Z.exports,yt="";var et=u({extends:"k-blocks",name:"k-blocks",mixins:[f],props:{_devInfo:{type:String,default:"Warning: I'm not the default k-blocks. I have been replaced by a k-translated-blocks !"}},mounted(){this.isWithinTranslatedComponent&&this.invertCustomAndNativeFunctions(["choose","chooseToConvert","onKey","onPaste","paste","pasteboard","remove","removeAll","convert","move","copy","copyAll","duplicate","chooseToConvert","add","removeAll","removeSelected"])},methods:{chooseCustom(s){return this.layoutEditingIsDisabled?null:this.chooseNative(s)},chooseToConvertCustom(s){return this.layoutEditingIsDisabled?null:this.chooseToConvertNative(s)},onKeyCustom(s,t=null){if(this.layoutEditingIsDisabled){this.isMultiSelectKey=!1;return}this.onKeyNative(s,t)},onPasteCustom(s){return this.layoutEditingIsDisabled?(s.preventDefault(),s.stopImmediatePropagation(),!1):this.pasteNative(s)},async pasteCustom(s){return this.layoutEditingIsDisabled?(s.preventDefault(),s.stopImmediatePropagation(),!1):this.pasteNative(s)},pasteboardCustom(){return this.layoutEditingIsDisabled?!1:this.pasteboardNative()},removeCustom(s){return this.layoutEditingIsDisabled?null:this.removeNative(s)},async convertCustom(s,t){return this.layoutEditingIsDisabled?null:this.convertNative(s,t)},chooseToConvert(s){return this.layoutEditingIsDisabled?null:this.chooseToConvert(s)},moveCustom(s){return this.layoutEditingIsDisabled?null:this.moveNative(s)},copyCustom(s){return this.layoutEditingIsDisabled?null:this.copyNative(s)},copyAllCustom(){return this.layoutEditingIsDisabled?null:this.copyAllNative()},async duplicateCustom(s,t){return this.layoutEditingIsDisabled?null:this.duplicateNative(s,t)},async addCustom(s="text",t){return this.layoutEditingIsDisabled?null:this.addNative(s,t)},removeAllCustom(){return this.layoutEditingIsDisabled?null:this.removeAllNative()},removeSelectedCustom(){return this.layoutEditingIsDisabled?null:this.removeSelectedNative()}}},null,null,!1,null,null,null,null);const st=et.exports,gt="",nt={extends:"k-block",mixins:[f],props:{_devInfo:{type:String,default:"Warning: I'm not the default k-block. I have been replaced by a k-translated-block !"}}};var it=function(){var t=this,n=t._self._c;return n("div",{ref:"container",class:["k-block-container","k-block-container-fieldset-"+t.type,t.containerType?"k-block-container-type-"+t.containerType:"",t.$attrs.class],style:t.$attrs.style,attrs:{"data-disabled":t.isDisabled,"data-hidden":t.isHidden,"data-id":t.id,"data-last-selected":t.isLastSelected,"data-selected":t.isSelected,"data-translate":t.fieldset.translate,tabindex:t.isDisabled?null:0},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"j",void 0,e.key,void 0)||!e.ctrlKey?null:(e.preventDefault(),e.stopPropagation(),t.$emit("merge"))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])||!e.ctrlKey||!e.altKey?null:(e.preventDefault(),e.stopPropagation(),t.$emit("selectDown"))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])||!e.ctrlKey||!e.altKey?null:(e.preventDefault(),e.stopPropagation(),t.$emit("selectUp"))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])||!e.ctrlKey||!e.shiftKey?null:(e.preventDefault(),e.stopPropagation(),t.$emit("sortDown"))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])||!e.ctrlKey||!e.shiftKey?null:(e.preventDefault(),e.stopPropagation(),t.$emit("sortUp"))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"backspace",void 0,e.key,void 0)||!e.ctrlKey?null:(e.stopPropagation(),t.backspace.apply(null,arguments))}],focus:function(e){return e.stopPropagation(),t.onFocus.apply(null,arguments)},focusin:function(e){return e.stopPropagation(),t.onFocusIn.apply(null,arguments)}}},[n("div",{staticClass:"k-block",class:t.className,attrs:{"data-disabled":t.isDisabled}},[n(t.customComponent,t._g(t._b({ref:"editor",tag:"component",attrs:{tabs:t.tabs}},"component",t.$props,!1),t.listeners))],1),t.layoutEditingIsDisabled&&t.isEditable?n("k-dropdown",{staticClass:"k-toolbar k-block-options"},[t.isEditable?n("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("edit"),icon:"edit"},on:{click:function(e){return t.listenersForOptions.open()}}}):t._e()],1):t.isDisabled?t._e():n("k-block-options",t._g(t._b({ref:"options"},"k-block-options",{isBatched:t.isBatched,isEditable:t.isEditable,isFull:t.isFull,isHidden:t.isHidden,isMergable:t.isMergable,isSplitable:t.isSplitable()},!1),t.listenersForOptions))],1)},ot=[],lt=u(nt,it,ot,!1,null,null,null,null);const at=lt.exports;panel.plugin("daandelange/translatedlayout",{components:{"k-block":at,"k-blocks":st},fields:{translatedlayout:V,translatedblocks:tt}})})(); 2 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 'MIT' 16 | ], 17 | version: '1.0.5', 18 | extends: [ 19 | 20 | 'fields' => [ 21 | // Undocumented, but identical to kirby's blocks and layout registering 22 | // Strings are checked against classes then instanciated. 23 | // See: https://github.com/getkirby/kirby/issues/3961 24 | 'translatedlayout' => 'TranslatedLayoutField', 25 | 'translatedblocks' => 'TranslatedBlocksField', 26 | ], 27 | 'fieldMethods' => [ 28 | 29 | // Content field method to retrieve the content as toLayouts 30 | 'toTranslatedLayouts' => function(\Kirby\Content\Field $field) : \Kirby\Cms\Layouts { 31 | // Native behaviour 32 | if( 33 | // Single lang has normal behaviour 34 | ( $field->model()->kirby()->multilang() === false ) || 35 | // Default lang has normal behaviour. 36 | ( $field->model()->translation()->language()->isDefault() ) 37 | ){ 38 | return $field->toLayouts(); 39 | } 40 | // Translation behaviour 41 | $returnLayouts = [];//null; 42 | 43 | // Grab primary language content 44 | $defaultLangCode = $field->model()->kirby()->defaultLanguage()->code(); 45 | $defaultLangTranslation = $field->model()->translation($defaultLangCode); 46 | 47 | // Check content 48 | if( !$defaultLangTranslation || !$defaultLangTranslation->version()->exists() ){ 49 | $returnLayouts = []; 50 | } 51 | // When the field doesn't exist in default lang : exit early 52 | else if(!array_key_exists($field->key(), $defaultLangTranslation->content())){ 53 | $returnLayouts = []; 54 | } 55 | 56 | // Grab translation data 57 | $translationValue = Kirby\Data\Data::decode($field->value(), type: 'json', fail: false); 58 | 59 | // Continue with translation data ? 60 | if(!$returnLayouts){ 61 | 62 | // Fetch primary lang data 63 | $defaultLangValue = \Kirby\Data\Data::decode($defaultLangTranslation->content()[$field->key()], type: 'json', fail: false)??[]; 64 | 65 | // Start with primary language content (fallback return) 66 | $returnLayoutsObj = \Kirby\Cms\Layouts::factory($defaultLangValue, ['parent' => $field->parent(), 'field'=>$field]); 67 | $returnLayouts = $returnLayoutsObj->toArray(); 68 | 69 | // Check translation data 70 | if( 71 | $translationValue && is_array($translationValue) && // Got data ? 72 | array_key_exists(TranslatedLayoutField::BLOCKS_KEY, $translationValue) && array_key_exists(TranslatedLayoutField::LAYOUTS_KEY, $translationValue) && // Got expected columns ? 73 | is_array($translationValue[TranslatedLayoutField::BLOCKS_KEY]) && is_array($translationValue[TranslatedLayoutField::LAYOUTS_KEY]) // Are they arrays ? 74 | ){ 75 | // Inject translation data 76 | $bp = getFieldBlueprintSelf($field, false); // <-- unparsed !!! 77 | 78 | // Parse fieldsets to know translation config (from user blueprint or field defaults) 79 | $bpFieldsets = array_key_exists('fieldsets', $bp)?$bp['fieldsets']:null; // Note: null triggers default fieldset 80 | $fieldsets = Fieldsets::factory($bpFieldsets, [ 81 | 'parent' => $field->parent(), 82 | 'field' => $field, 83 | ]); 84 | 85 | // Like Kirby\Form\Layout::setSettings(); 86 | // $settings = $column['attrs']; 87 | $attrsFieldSet = null; 88 | if(array_key_exists('settings', $bp) && array_key_exists('fields', $bp['settings'])){ 89 | $settings = $bp['settings'];//['fields']; 90 | $settings['type'] = 'layout'; 91 | $settings['parent'] = $field->parent(); 92 | $attrsFieldSet = Fieldset::factory($settings); 93 | } 94 | 95 | // Convert index keys to id keys (removed by blocksToValues() on save) 96 | $translationValue[TranslatedLayoutField::BLOCKS_KEY] = array_column($translationValue[TranslatedLayoutField::BLOCKS_KEY], null, 'id'); 97 | $translationValue[TranslatedLayoutField::LAYOUTS_KEY] = array_column($translationValue[TranslatedLayoutField::LAYOUTS_KEY], null, 'id'); 98 | 99 | //$bp = getFieldBlueprint(); 100 | $returnLayouts = syncLanguages($returnLayouts, $translationValue, $fieldsets, $attrsFieldSet); 101 | } 102 | else { 103 | // Ignore: incorrect translation data 104 | } 105 | } 106 | 107 | return \Kirby\Cms\Layouts::factory($returnLayouts, ['parent' => $field->parent(), 'field'=> $field]); 108 | }, 109 | 110 | ], 111 | 'blueprints' => [ 112 | 113 | // Todo: Possible issue = when these blocks are not registered in the user blueprint, they get added. NVM they are just defaults. 114 | 'fields/translatedlayoutwithfieldsetsnative' => __DIR__ . '/src/blueprints/fields/translatedlayoutwithfieldsets.yml', 115 | 'fields/translatedlayoutwithfieldsets' => function ($kirby) { // Todo: rename this to translatedlayoutwithfieldsettranslations 116 | // Put all static definitions in an yml file so it's easier to copy/paste/write. 117 | // From Kirby/Cms/Blueprint.php in function find() 118 | 119 | // Query existing blocks 120 | $blockBlueprints = $kirby->blueprints('blocks'); 121 | 122 | return array_merge( 123 | 124 | // Load static properties from file 125 | Data::read( __DIR__ . '/src/blueprints/fields/translatedlayoutwithfieldsets.yml' ), 126 | 127 | // Dynamically inject non-default blocks depending on installed addons 128 | // Todo: add more translation settings for community blocks 129 | 130 | // Inject support for some block plugins 131 | // Feel free to add the structure of your addon and submit a PR 132 | (in_array('woo/localvideo', $blockBlueprints) ? [ 133 | 'translate' => false, 134 | 'tabs' => [ 135 | 'source' => [ 136 | 'fields' => [ 137 | 'vidfile' => [ 138 | 'translate' => false, 139 | ], 140 | 'vidposter' => [ 141 | 'translate' => false, 142 | ], 143 | ], 144 | ], 145 | 'settings' => [ 146 | 'fields' => [ 147 | 'class' => [ 148 | 'translate' => false, 149 | ], 150 | 'controls' => [ 151 | 'translate' => false, 152 | ], 153 | 'mute' => [ 154 | 'translate' => false, 155 | ], 156 | 'autoplay' => [ 157 | 'translate' => false, 158 | ], 159 | 'loop' => [ 160 | 'translate' => false, 161 | ], 162 | 'playsinline' => [ 163 | 'translate' => false, 164 | ], 165 | 'preload' => [ 166 | 'translate' => false, 167 | ], 168 | ], 169 | ], 170 | ] 171 | ] : []) 172 | ); 173 | } 174 | ], 175 | ] // end: extends array 176 | ); 177 | -------------------------------------------------------------------------------- /kirbyup.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'kirbyup/config' 3 | 4 | export default defineConfig({ 5 | alias: { 6 | '@KirbyPanel/': `${resolve(__dirname, '../../../kirby/panel/src')}/`, 7 | //'@/' : `${resolve(__dirname, '../../../kirby/panel/src')}/`, // Uncomment to resolve panel files further 8 | }, 9 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TranslatedLayout", 3 | "version": "1.0.5", 4 | "description": "A layout field with embedded translation logic.", 5 | "main": "index.js", 6 | "author": "Daan de Lange", 7 | "license": "MIT", 8 | "repository": "https://github.com/daandelange/kirby-translatedlayout", 9 | "scripts": { 10 | "dev": "kirbyup src/index.js --watch", 11 | "build": "kirbyup src/index.js" 12 | }, 13 | "devDependencies": { 14 | "kirbyup": "^2.1.1", 15 | "esbuild-wasm": "^0.15.16" 16 | }, 17 | "overrides": { 18 | "esbuild": "npm:esbuild-wasm@latest" 19 | }, 20 | "pnpm": { 21 | "overrides": { 22 | "esbuild": "npm:esbuild-wasm@latest" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/blueprints/fields/translatedlayoutwithfieldsets.yml: -------------------------------------------------------------------------------- 1 | 2 | type: translatedlayout 3 | #title: Layout 4 | translate: true 5 | # Default fieldsets can be found in Kirby/Cms/Fieldsets in the factory() function 6 | # Individual definitions can be found in kirby/config/blocks/type/type.yml 7 | # Feel free to copy/paste this default set to your blueprints 8 | # All translate definitions are filled to ensure over-riding all defaults 9 | fieldsets: 10 | code: 11 | extends: blocks/code 12 | translate: true 13 | fields: 14 | code: 15 | translate: true 16 | language: 17 | translate: false 18 | gallery: 19 | extends: blocks/gallery 20 | translate: false 21 | fields: 22 | images: 23 | translate: false 24 | heading: 25 | extends: blocks/heading 26 | translate: true 27 | fields: 28 | level: 29 | translate: false 30 | text: 31 | translate: true 32 | image: 33 | extends: blocks/image 34 | translate: true 35 | fields: 36 | location: 37 | translate: false 38 | image: 39 | translate: false 40 | src: 41 | translate: false 42 | alt: 43 | translate: true 44 | caption: 45 | translate: true 46 | link: 47 | translate: false 48 | ratio: 49 | translate: false 50 | crop: 51 | translate: false 52 | line: 53 | extends: blocks/line 54 | translate: false 55 | list: 56 | extends: blocks/list 57 | translate: true 58 | fields: 59 | text: 60 | translate: true 61 | markdown: 62 | extends: blocks/markdown 63 | translate: true 64 | fields: 65 | text: 66 | translate: true 67 | quote: 68 | extends: blocks/quote 69 | translate: true 70 | fields: 71 | text: 72 | translate: true 73 | citation: 74 | translate: true 75 | text: 76 | extends: blocks/text 77 | translate: true 78 | fields: 79 | text: 80 | translate: true 81 | video: 82 | extends: blocks/video 83 | translate: true 84 | fields: 85 | text: 86 | translate: true 87 | url: 88 | translate: false 89 | table: 90 | extends: blocks/table 91 | translate: true -------------------------------------------------------------------------------- /src/classes/TranslatedBlockTraits.php: -------------------------------------------------------------------------------- 1 | kirby()->multilang() || !$model->kirby()->language() || $model->kirby()->language()->isDefault()){ 16 | parent::setFieldsets($fieldsets, $model);// added this line compared to native 17 | return; 18 | } 19 | 20 | if (is_string($fieldsets) === true) { 21 | $fieldsets = []; 22 | } 23 | 24 | $fieldsets = $this->adaptFieldsetsToTranslation($fieldsets);// added this line compared to native 25 | 26 | // Todo : if fieldsets is null, factory() seems to set it to a default set, causing disabled not to be set correctly... 27 | $this->fieldsets = Fieldsets::factory($fieldsets, [ 28 | 'parent' => $model 29 | ]); 30 | } 31 | 32 | // Adds translation statuses to all fields and modifies them according to blueprint. 33 | private static function adaptFieldsetsToTranslation(?array $fieldsets) : ?array { 34 | if($fieldsets) foreach($fieldsets as $key => &$fieldset){ 35 | $fieldset = static::adaptFieldsetToTranslation($fieldset); 36 | 37 | // Todo: can it happen that groups contain more fieldsets ? They might need a dedicated if()... 38 | } 39 | return $fieldsets; 40 | } 41 | 42 | // Force-disables non-translateable fields 43 | private static function adaptFieldsetToTranslation(null|string|array $fieldset) : null|string|array { 44 | // Set translations ? 45 | // Already set via blueprint YML ? if using: "extends: translatedlayoutwithfields" ? Ensure to set defaults ? 46 | 47 | // Blueprint is not yet expanded here. Todo: What if the default should be translateable ? 48 | if(is_string($fieldset) === true){ 49 | return $fieldset; // Leave as is, no translation logic for now ! 50 | } 51 | 52 | // Set disabed ? Saveable ? if translate is false. So the field is disabled for editing in panel 53 | if($fieldset && isset($fieldset['translate']) && $fieldset['translate'] === false ){ 54 | $fieldset['disabled']=true; 55 | //$fieldset['saveable']=false; // Assumes the field has no value ! Not possible 56 | } 57 | 58 | return $fieldset; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/classes/TranslatedBlocksField.php: -------------------------------------------------------------------------------- 1 | classname is automatically converted from class otherwise, which grabs the wrong component in the panel 33 | return 'translatedblocks'; 34 | } 35 | 36 | public function store($value){ // Returns the (array) value to store (string). The value has been fill()ed already. 37 | return parent::store($value); 38 | } 39 | 40 | // Flattens blocks by placing the ID as key for easier lookup. 41 | private static function flattenBlocks( BlocksCollection $blocks) : array { 42 | $flatStructure = []; 43 | if( !$blocks->isEmpty() ){ 44 | foreach ($blocks->toArray() as $blockIndex => $block){ 45 | // We should have: $block.id , $block.content , $block.type, $block.isHidden 46 | $keyB = $block['id']??('block_'.$blockIndex); 47 | if(isset($flatStructure[$keyB])) 48 | throw new LogicException("Ouch, now unique IDs can exist twice ! I can't handle this."); // Todo: auto-incremment instead of throwing ? Will not fix translations syncing but can prevent losing block-translations in the content file for manual restore ? 49 | 50 | $flatStructure[$keyB] = $block; 51 | } 52 | } 53 | return $flatStructure; 54 | } 55 | 56 | // Value setter (used in construct, save, display, etc) // opposite of store() ? (also used before store to recall js values) 57 | // Note : Panel.page.save passes an array while loadFromContent passes a yaml string. 58 | public function fill(mixed $value = null): static { 59 | 60 | // Default lang uses native kirby code, which is faster. :) 61 | if( 62 | // Single lang has normal behaviour 63 | ( $this->kirby()->multilang() === false ) || 64 | // Default lang has normal behaviour. 65 | ( $this->model()->translation()->language()->isDefault() ) || 66 | // if attrs.translate is set to false 67 | ( $this->translate() === false ) 68 | ){ 69 | parent::fill($value); 70 | return $this; 71 | } 72 | 73 | // Fetch translation 74 | $value = $this->flattenBlocks(BlocksCollection::factory(BlocksCollection::parse($value))); // todo: we don't need the BlocksCollection, just the array would be fine ? 75 | 76 | // Check default lang for this model (should always exist anyways) 77 | $defaultLang = $this->kirby()->defaultLanguage()->code(); 78 | $currentLang = $this->kirby()->language()->code();// $this->model()->translation()->code();// commented is more correct, but loads translation strings = useless here 79 | 80 | $defaultLangTranslation = $this->model()->translation($defaultLang); 81 | if( !$defaultLangTranslation || !$defaultLangTranslation->version()->exists() ){ 82 | throw new LogicException('Multilanguage is enabled but there is no content for the default language... who\'s the wizzard ?!'); 83 | } 84 | 85 | // Fetch default lang 86 | $defaultLangValue = BlocksCollection::parse( $defaultLangTranslation->content()[$this->name()] ?? [] ); 87 | $defaultLangBlocks = BlocksCollection::factory( $defaultLangValue, ['parent' => $this->model] )->toArray(); 88 | 89 | // Start sanitizing / Syncing the structure 90 | 91 | // Loop blocks and restrict them to the default language 92 | foreach( $defaultLangBlocks as $blockIndex => &$block){ 93 | $blockID = $block['id']??$blockIndex; 94 | // Note: If code breaks: Useful inspiration for syncing translations --> ModelWithContent.php [in function content()] : 95 | 96 | // Get blueprint block attribtes 97 | try { 98 | $blockBlueprint = $this->fieldset($block['type']); 99 | } catch (Throwable) { 100 | // skip invalid block translations 101 | continue; 102 | } 103 | 104 | $translateByDefault = true; // todo: parse this from a plugin option ? 105 | 106 | // Translateable and translation available ? 107 | if(($blockBlueprint->translate() || $translateByDefault) && array_key_exists($blockID, $value) && array_key_exists('content', $value[$blockID]) ){ 108 | 109 | // Loop blueprint fields here (not defaultLanguage values) to enable translations not in the default lang ? 110 | try { 111 | $blockFields = $blockBlueprint->fields() ?? []; // todo : Cache fields, like in BlocksField.php::validations() 112 | } catch (Throwable) { 113 | // skip invalid block translations 114 | continue; 115 | } 116 | foreach($blockFields as $fieldName => $fieldOptions){ 117 | // Translate if field's translation is explicitly set or if the block is set to translate 118 | $translateField = array_key_exists('translate', $fieldOptions) ? ($fieldOptions['translate'] === true) : ($translateByDefault && $blockBlueprint->translate()); 119 | if( 120 | // Is the field translateable ? 121 | $translateField 122 | 123 | // Got keys in both contentTranslations ? 124 | && array_key_exists($fieldName, $block['content']) // Note : Useless condition? prevents retrieving translations that don't exist in default lang content ? 125 | && array_key_exists($fieldName, $value[$blockID]['content']) 126 | // todo: add empty condition on translation ? This brobably should take a blueprint option if translateing empty values. Leaving translations empty can also be useful 127 | //&& !V::empty($layouts[$layoutIndex]['columns'][$columnIndex]['blocks'][$blockIndex]['content']) 128 | ){ 129 | // Replace the default lang block content with the translated one. 130 | $block['content'][$fieldName] = $value[$blockID]['content'][$fieldName]; 131 | 132 | // Todo : Handle nested fields in a field ? ? ? (structure, etc...) 133 | } 134 | } 135 | } 136 | } 137 | 138 | // Reset keys 139 | $defaultLangBlocks = $this->blocksToValues($defaultLangBlocks);//static::keysToIndexes($defaultLangBlocks); 140 | 141 | // Remember value 142 | $this->value = $defaultLangBlocks; 143 | $this->errors = null; 144 | return $this; 145 | } 146 | 147 | // Override parent routes to intercept pastes 148 | public function routes(): array { 149 | $field = $this; 150 | $parentRoutes = parent::routes(); 151 | foreach($parentRoutes as $key => &$route){ 152 | if($route['pattern']==='paste'){ 153 | $prevAction = $route['action']; 154 | $route['action'] = function () use ($field, $prevAction) { 155 | // Simply disable pasting blocks in translations. 156 | if($this->kirby()->language()->isDefault()){ 157 | try{ 158 | $newBlocks = $prevAction->call($this, $field); 159 | } 160 | catch(Throwable $e){ 161 | throw new Exception('Could not call native route action : '.$e->getMessage()); 162 | } 163 | 164 | foreach($newBlocks as $index => &$newBlock){ 165 | $pastedBlockUuid = $newBlock['id']; 166 | // If the same is returned, the panel will generate a new one which will break translations sync 167 | // Wrong : The panel always generates a new UUID ! 168 | $newBlock['id'] = Str::uuid(); 169 | // Todo : Use $pastedBlockUuid to duplicate it in translations so the paste applies to all langs. 170 | } 171 | 172 | return $newBlocks; 173 | } 174 | else throw new Exception('Pasting has been disabled in translations, but could be implemented !'); 175 | }; 176 | } 177 | // todo: modify fieldset api route too 178 | } 179 | return $parentRoutes; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/classes/TranslatedLayoutField.php: -------------------------------------------------------------------------------- 1 | setTranslate(false);//$params['translate'] ?? false); 39 | } 40 | 41 | const string LAYOUTS_KEY = 'layouts'; 42 | const string BLOCKS_KEY = 'blocks'; 43 | const array EMPTY_VALUE = [ 44 | 'layouts' => [], 45 | 'blocks' => [], 46 | ]; 47 | 48 | // Extend layout mixin (todo: still needed ??) 49 | // 'extends' => 'layout', // No works... 50 | public function extends(){ 51 | return 'layout'; 52 | } 53 | 54 | /** 55 | * Returns the field type 56 | * 57 | * @return string 58 | */ 59 | public function type(): string { 60 | // Needs uppercase, see FieldClass.php::type() --> classname is automatically converted from class otherwise, which grabs the wrong component in the panel 61 | return 'translatedlayout'; 62 | } 63 | 64 | // public function props() : array { // from/in-sync-with the blueprint 65 | // return array_merge(parent::props(), [ 66 | // //'empty' => $this->empty(), 67 | // //'translate' => false, 68 | // //'disabled' => true, // Disabled state for the layouts field, disables adding/removing layouts. BUT disables all contained blocks too. Needs to be modified within the field component. 69 | // //'type' => 'translatedlayout', // dunno why, php sets it to translatedLayout.... 70 | // ]); 71 | // } 72 | 73 | // // Replaces numbered indexes by a string from item[$key]. 74 | // public static function indexesToKeys(array $array, string $key='id'): array { 75 | // $ret = []; 76 | // foreach ($array as $layoutKey => $layoutValue) { 77 | // $layoutKey = $layoutValue['id']??$layoutKey; 78 | // $ret[$layoutKey]=$layoutValue; 79 | 80 | // if(array_key_exists('columns', $ret[$layoutKey])){ 81 | // foreach ($ret[$layoutKey]['columns'] as $columnKey => $columnValue) { 82 | // unset($ret[$layoutKey]['columns'][$columnKey]); 83 | // $columnKey = $columnValue['id']??$columnKey; 84 | // $ret[$layoutKey]['columns'][$columnKey]=$columnValue; 85 | 86 | // if(array_key_exists('blocks', $ret[$layoutKey]['columns'][$columnKey])){ 87 | // foreach ($ret[$layoutKey]['columns'][$columnKey]['blocks'] as $blockKey => $blockValue) { 88 | // unset($ret[$layoutKey]['columns'][$columnKey]['blocks'][$blockKey]); 89 | // $blockKey = $blockValue['id']??$blockKey; 90 | // $ret[$layoutKey]['columns'][$columnKey]['blocks'][$blockKey]=$blockValue; 91 | // } 92 | // } 93 | // } 94 | // } 95 | // } 96 | // return $ret; 97 | // } 98 | 99 | // // Replaces named keys to numbered indexes. 100 | // public static function keysToIndexes(array $array, string $key='id'): array { 101 | 102 | // foreach ($array as $layoutKey => $layoutValue) { 103 | // //$array[$layoutKey][$key]=$layoutKey; // Sync key with id 104 | // if(array_key_exists('columns', $array[$layoutKey])){ 105 | // foreach ($array[$layoutKey]['columns'] as $columnKey => $columnValue) { 106 | // //$array[$layoutKey]['columns'][$columnKey][$key]=$columnKey; // Sync key with id 107 | // if(array_key_exists('blocks', $array[$layoutKey]['columns'][$columnKey])){ 108 | // foreach ($array[$layoutKey]['columns'][$columnKey]['blocks'] as $blockKey => $blockValue) { 109 | // //$array[$layoutKey]['columns'][$columnKey]['blocks'][$blockKey][$key]=$blockKey; // Sync key with id 110 | // } 111 | // $array[$layoutKey]['columns'][$columnKey]['blocks'] = array_values($array[$layoutKey]['columns'][$columnKey]['blocks']); // remove blocks keys 112 | // } 113 | // } 114 | // $array[$layoutKey]['columns'] = array_values($array[$layoutKey]['columns']); // remove columns keys 115 | // } 116 | // } 117 | // $array = array_values($array); // remove keys on level 1 118 | // return $array; 119 | // } 120 | 121 | // k3 code, depreciated k5 ! 122 | // public function store($value){ // Returns the (array) value to store (string). The value has been fill()ed already. 123 | // return parent::store($value); 124 | // } 125 | 126 | // Flattens a layout. All blocks, columns and layouts are in their own array. 127 | protected static function flattenLayoutsColumnsBlocks( Kirby\Cms\Layouts $layouts/*, array $columns = ['layouts','columns','blocks']*/ ) : array { 128 | $flatStructure = static::EMPTY_VALUE; 129 | if( !$layouts->isEmpty() ){ 130 | foreach ($layouts->toArray() as $layoutIndex => $layout) { 131 | // We should have: $layout.id , $layout.columns , $layout.attrs 132 | if( isset($layout['columns']) ){ 133 | foreach($layout['columns'] as $columnIndex => $column) { 134 | // We should have: $column.id , $column.blocks , $column.width 135 | if( isset($column['blocks']) ){ 136 | foreach( $column['blocks'] as $blockIndex => $block){ 137 | // We should have: $block.id , $block.content , $block.type, $block.isHidden 138 | $keyB = $block['id']??('block_'.$layoutIndex.'_'.$columnIndex.'_'.$blockIndex); 139 | if(isset($flatStructure['blocks'][$keyB]) || isset($flatStructure['columns'][$keyB])) { 140 | // In default lang, generate new ID if numerical OR 141 | throw new LogicException("Ouch, now unique IDs can exist twice ! I can't handle this, please fix any duplicate ID in your content file. (duplicate block of type ".($block['type']??'Unknown')." with ID: ".$block['id'].")."); 142 | } 143 | 144 | $flatStructure['blocks'][$keyB] = $block; 145 | } 146 | unset($column['blocks']); 147 | } 148 | // $keyC = $column['id']??$columnIndex; 149 | // $flatStructure['columns'][$keyC] = $column; 150 | } 151 | unset($layout['columns']); 152 | } 153 | $keyL = $layout['id']??('layout_'.$layoutIndex); 154 | if(isset($flatStructure['blocks'][$keyL]) || isset($flatStructure['columns'][$keyL])) 155 | throw new LogicException("Ouch, now unique IDs can exist twice ! I can't handle this."); 156 | $flatStructure['layouts'][$keyL] = $layout; // Note: Attrs are simply copied within 157 | } 158 | } 159 | return $flatStructure; 160 | } 161 | 162 | /** 163 | * Returns the value of the field in a format 164 | * to be stored by our storage classes 165 | */ 166 | public function toStoredValue(bool $default = false): mixed 167 | { 168 | 169 | // Default lang uses native kirby code, which is faster & won't break. :) 170 | if( 171 | // Single lang has normal behaviour 172 | ( $this->kirby()->multilang() === false ) || 173 | // Default lang has normal behaviour. 174 | ( $this->model()->translation()->language()->isDefault() ) || 175 | // if attrs.translate is set to false 176 | ( $this->translate() === false ) 177 | ){ 178 | return parent::toStoredValue($default); 179 | } 180 | 181 | // Original Kirby behaviour, modified 182 | $value = $this->toFormValue($default); 183 | $valueLayout = Layouts::factory($value, ['parent' => $this->model]); 184 | 185 | // returns empty string to avoid storing empty array as string `[]` 186 | // and to consistency work with `$field->isEmpty()` 187 | if ($valueLayout->toArray() === []) { 188 | return ''; 189 | return static::EMPTY_VALUE; 190 | } 191 | 192 | $value = $this->flattenLayoutsColumnsBlocks($valueLayout); 193 | 194 | // Convert full data to stored data (more compact) 195 | $layoutsIndexed = $value[TranslatedLayoutField::LAYOUTS_KEY]; 196 | $value[TranslatedLayoutField::LAYOUTS_KEY] = []; 197 | foreach ($layoutsIndexed as $layoutIndex => $layout) { 198 | if (array_key_exists('attrs', $layout) && ($layout['attrs']!==[] && $layout['attrs'] !== null)) { // Changed line 199 | $layout['attrs'] = $this->attrsForm($layout['attrs'])->content(); 200 | } 201 | $value[TranslatedLayoutField::LAYOUTS_KEY][] = $layout; // Save without array key 202 | } 203 | // Blocks too 204 | $value[TranslatedLayoutField::BLOCKS_KEY] = $this->blocksToValues($value[TranslatedLayoutField::BLOCKS_KEY] ?? [], 'content'); 205 | 206 | // Remove untranslateable data 207 | if(array_key_exists(static::BLOCKS_KEY, $value)){ 208 | foreach($value[static::BLOCKS_KEY] as $blockID => &$block){ 209 | $blockType = $block['type']; 210 | // Build new value to only keep known fields 211 | $blockContent = []; 212 | if($blockFieldset = $this->fieldsets()->find($blockType)){ 213 | // Apply translation to fields within 214 | if($blockFieldset->translate()===true){ 215 | // Translate block fields 216 | foreach($blockFieldset->fields() as $fieldName => $field){ 217 | // Only keep when translateable and not empty 218 | if($field['translate']===true && !V::empty($value[static::BLOCKS_KEY][$blockID]['content'][$fieldName])){ 219 | //if($fieldName=='alt') xdebug_break(); 220 | $blockContent[$fieldName] = $value[static::BLOCKS_KEY][$blockID]['content'][$fieldName]; 221 | continue; 222 | } 223 | 224 | } 225 | 226 | // Keep whole block 227 | continue; 228 | } 229 | } 230 | 231 | // Keep minimal data 232 | if(count($blockContent)>=1){ 233 | $value[static::BLOCKS_KEY][$blockID]['content']=$blockContent; 234 | } 235 | // ignore unknown and untranslateable blocks in translations 236 | else { 237 | unset($value[static::BLOCKS_KEY][$blockID]); 238 | } 239 | } 240 | } 241 | if(array_key_exists(static::LAYOUTS_KEY, $value)){ 242 | foreach($value[static::LAYOUTS_KEY] as $layoutID => &$layout){ 243 | // Build new value to only keep known fields 244 | $settingsContent = []; 245 | 246 | if($settingsFieldset = $this->settings()){ 247 | 248 | // Apply translation to fields within 249 | if($settingsFieldset->translate()===true){ 250 | // Translate block fields 251 | foreach($settingsFieldset->fields() as $fieldName => $field){ 252 | 253 | // Only keep when translateable and not empty 254 | if($field['translate']===true && !V::empty($value[static::LAYOUTS_KEY][$layoutID]['attrs'][$fieldName])){ 255 | $settingsContent[$fieldName] = $value[static::LAYOUTS_KEY][$layoutID]['attrs'][$fieldName]; 256 | continue; 257 | } 258 | } 259 | 260 | // Keep whole block 261 | continue; 262 | } 263 | } 264 | 265 | // Keep minimal data 266 | if(count($settingsContent)>=1){ 267 | $value[static::LAYOUTS_KEY][$layoutID]['attrs']=$settingsContent; 268 | } 269 | // ignore unknown and untranslateable blocks in translations 270 | else { 271 | unset($value[static::LAYOUTS_KEY][$layoutID]); 272 | } 273 | } 274 | } 275 | 276 | // Changed lines 277 | // Keep translations only. 278 | //$value = $this->flattenLayoutsColumnsBlocks(Layouts::factory($this->toFormValue($default), ['parent' => $this->model])); 279 | 280 | // Original return 281 | return \Kirby\Data\Json::encode($value, pretty: $this->pretty()); 282 | } 283 | 284 | /** 285 | * Returns the value of the field in a format to be used in forms 286 | * (e.g. used as data for Panel Vue components) 287 | */ 288 | public function toFormValue(bool $default = false): mixed 289 | { 290 | // Default lang uses native kirby code, which is faster & won't break. :) 291 | if( 292 | // Single lang has normal behaviour 293 | ( $this->kirby()->multilang() === false ) || 294 | // Default lang has normal behaviour. 295 | ( $this->model()->translation()->language()->isDefault() ) || 296 | // if attrs.translate is set to false 297 | ( $this->translate() === false ) 298 | ){ 299 | return parent::toFormValue($default); 300 | } 301 | 302 | // Original below : 303 | if ($this->hasValue() === false) { 304 | return null; 305 | } 306 | 307 | if ($default === true && $this->isEmpty() === true) { 308 | return $this->default(); 309 | } 310 | 311 | return $this->value; 312 | } 313 | // Value setter (used in construct, save, display, etc) // opposite of store() ? (also used before store to recall js values) 314 | // Note : Panel.page.save passes an array while loadFromContent passes a yaml string. 315 | // Note: Only called from the panel, not in frontend ! 316 | public function fill(mixed $value): static { 317 | 318 | // Default lang uses native kirby code, which is faster & won't break. :) 319 | if( 320 | // Single lang has normal behaviour 321 | ( $this->kirby()->multilang() === false ) || 322 | // Default lang has normal behaviour. 323 | ( $this->model()->translation()->language()->isDefault() ) || 324 | // if attrs.translate is set to false 325 | ( $this->translate() === false ) 326 | ){ 327 | return parent::fill($value); 328 | } 329 | 330 | // We got a translation ! 331 | 332 | // Format incoming raw data to flattened blocks and layouts 333 | 334 | // Decode string to data 335 | // Ex: The value comes from the content file which only stores the translations 336 | // Ex: Or we got a default value from the blueprint (which we haven't changed : it's the default lang value) 337 | if( is_string($value) ){ 338 | 339 | // Simply convert the string to array 340 | $value = Kirby\Data\Data::decode($value, type: 'json', fail: false); 341 | } 342 | // The value is empty : Fill with empty data 343 | if( is_null($value) ){ 344 | $value = static::EMPTY_VALUE; 345 | // Exit early 346 | $this->value = $value; 347 | $this->errors = null; 348 | return $this; 349 | } 350 | 351 | // We got an array 352 | else if( is_array($value) ){ 353 | 354 | // Is the array already formatted ? 355 | if(array_key_exists(static::BLOCKS_KEY, $value) && array_key_exists(static::LAYOUTS_KEY, $value)){ 356 | // Secure 357 | // Fixme: Need to allow array AND null values ? 358 | if( !is_array($value[static::BLOCKS_KEY]) && !is_array($value[static::LAYOUTS_KEY]) ){ 359 | // Todo: on save, the value becomes null when throwing, which sets the stored value to null. The panel doesn't notify anything. 360 | // Maybe: Rather die and respond with a panel error if Or is there a way to handle this response natively ? 361 | throw new LogicException('The layout field received an unfamiliar array format, throwing to ensure everything is OK.'); 362 | } 363 | // Keep as is 364 | //$value = $value; 365 | 366 | // Convert index keys to id keys (removed by blocksToValues() on save) 367 | $value[static::BLOCKS_KEY] = array_column($value[static::BLOCKS_KEY], null, 'id'); 368 | $value[static::LAYOUTS_KEY] = array_column($value[static::LAYOUTS_KEY], null, 'id'); 369 | } 370 | // We got another array format 371 | // Assume it's in full form-data format 372 | // Ex: when the panel sends us back a full layout that we have to parse, probably for saveing it 373 | else { 374 | // Secure 375 | if( !empty($value) && ( !isset($value[0]) || !isset($value[0]['columns']) || !isset($value[0]['id']) ) ){ 376 | // Todo: on save, the value becomes null when throwing, which sets the stored value to null. The panel doesn't notify anything. 377 | // Maybe: Rather die and respond with a panel error if Or is there a way to handle this response natively ? 378 | throw new LogicException('The layout field received an unfamiliar array format, throwing to ensure everything is OK.'); 379 | } 380 | 381 | // Keep flattened 382 | $value = $this->flattenLayoutsColumnsBlocks( Layouts::factory($value, ['parent' => $this->model]) ); 383 | } 384 | } 385 | // Wrong data format 386 | else { 387 | // Todo: this could trigger when the save file is mistakenly hand-edited, or corrupted. Should this be a more gentle message ? 388 | throw new LogicException('Unrecognised translated layout value : Can\'t fill the field!'); 389 | $this->errors[] = 'Unrecognised translated layout value : Can\'t fill the field!'; 390 | //$this->value=...; 391 | return $this; 392 | } 393 | 394 | 395 | // Check values ? (at this point w have an array of ) 396 | if( !isset($value['layouts']) || !isset($value['blocks']) ){ 397 | throw new LogicException('The parsed data looks wrong. Aborting.'); 398 | } 399 | 400 | $flattenedLayouts = $value; 401 | 402 | // Todo : after some testing, the logic exceptions above and below could return the default language, just in case... 403 | 404 | // Check default lang for this model (should always exist anyways) 405 | $defaultLang = $this->kirby()->defaultLanguage()->code(); 406 | $currentLang = $this->kirby()->language()->code();// $this->model()->translation()->code();// commented is more correct, but loads translation strings = useless here 407 | 408 | $defaultLangTranslation = $this->model()->translation($defaultLang); 409 | if( !$defaultLangTranslation || !$defaultLangTranslation->version()->exists() ){ 410 | // Todo: rather return empty field ! 411 | throw new LogicException('Multilanguage is enabled but there is no content for the default language... who\'s the wizzard ?!'); 412 | } 413 | 414 | // Fetch default lang 415 | 416 | // When the field doesn't exist in default lang : exit early 417 | if(!array_key_exists($this->name(), $defaultLangTranslation->content())){ 418 | $this->errors=null; 419 | $this->value=[]; 420 | return $this; 421 | } 422 | 423 | $defaultLangValue = \Kirby\Data\Data::decode($defaultLangTranslation->content()[$this->name()], type: 'json', fail: false)??[]; 424 | $defaultLangLayouts = Layouts::factory($defaultLangValue, ['parent' => $this->model])->toArray(); 425 | // Start sanitizing / Syncing the structure 426 | 427 | // apply kirby functions 428 | if(true){ // Original sanitize functions (since k5) 429 | foreach ($defaultLangLayouts as $layoutIndex => $layout) { 430 | if ($this->settings !== null) { 431 | $defaultLangLayouts[$layoutIndex]['attrs'] = $this->attrsForm($layout['attrs'])->values(); 432 | } 433 | 434 | foreach ($layout['columns'] as $columnIndex => $column) { 435 | $defaultLangLayouts[$layoutIndex]['columns'][$columnIndex]['blocks'] = $this->blocksToValues($column['blocks']); 436 | } 437 | } 438 | } 439 | // Loop the default language's structure and let translation content replace it 440 | foreach ($defaultLangLayouts as $layoutIndex => &$layout) { // <-- Apply blockstovalues 441 | $layoutID = $layout['id']??$layoutIndex; 442 | 443 | // Check the layout settings / attrs 444 | if ($this->settings !== null) { // 445 | // Generate the corresponding form 446 | $attrForm = $this->attrsForm($layout['attrs']); 447 | 448 | // Load value from default lang 449 | $layout['attrs'] = $attrForm->values(); 450 | 451 | // Check for translations 452 | $attrFields = $attrForm->fields(); 453 | if( $attrFields->count() > 0 && array_key_exists($layoutID, $flattenedLayouts['layouts']) && array_key_exists('attrs', $flattenedLayouts['layouts'][$layoutID]) ){ 454 | 455 | // Loop default attrs by field 456 | foreach($attrFields as $fieldName => $attrField){ 457 | // Translate if needed 458 | if( 459 | $attrField->translate() === true // the field translates 460 | && array_key_exists($fieldName, $flattenedLayouts['layouts'][$layoutID]['attrs']) // The translation exists 461 | ){ 462 | $layout['attrs'][$fieldName] = $flattenedLayouts['layouts'][$layoutID]['attrs'][$fieldName]; 463 | // Todo : What if translation is empty ? 464 | // !V::empty($layouts[$layoutIndex]['attrs'][$attrIndex]) 465 | 466 | // Todo: also handle nested fields translation ? 467 | } 468 | } 469 | } 470 | } 471 | 472 | foreach($layout['columns'] as $columnIndex => &$column) { 473 | $columnID = $column['id']??$columnIndex; 474 | 475 | // Loop blocks and restrict them to the default language 476 | foreach( $column['blocks'] as $blockIndex => &$block){ 477 | $blockID = $block['id']??$blockIndex; 478 | // Note: If code breaks: Useful inspiration for syncing translations --> ModelWithContent.php [in function content()] : 479 | 480 | try { 481 | $blockBlueprint = $this->fieldset($block['type']); 482 | } catch (Throwable $e) { 483 | // skip invalid blocks 484 | // checkme: skipping leaves default translation. Is this the desired behaviour ? (probably prevents saving the field too!?) 485 | continue; 486 | } 487 | 488 | $translateByDefault = false; // todo: parse this from a plugin option ? 489 | 490 | // Translateable and translation available ? 491 | if(($blockBlueprint->translate() || $translateByDefault) && array_key_exists($blockID, $flattenedLayouts['blocks'])){ 492 | // Loop blueprint fields here (not defaultLanguage values) to enable translations not in the default lang 493 | //foreach($defaultLangLayouts[$layoutIndex]['columns'][$columnIndex]['blocks'][$blockIndex]['content'] as $fieldName => $fieldData){ 494 | foreach($blockBlueprint->fields() as $fieldName => $fieldOptions){ // Todo: fields() can throw ! 495 | // Translate if field's translation is explicitly set or if the block is set to translate 496 | $translateField = array_key_exists('translate', $fieldOptions) ? ($fieldOptions['translate'] === true) : ($translateByDefault && $blockBlueprint->translate()); 497 | if( 498 | // Is the field translateable ? 499 | $translateField 500 | 501 | // Got keys in both contentTranslations ? 502 | && array_key_exists($fieldName, $block['content']) 503 | && array_key_exists($fieldName, $flattenedLayouts['blocks'][$blockID]['content']) 504 | // todo: add empty condition on translation ? This brobably should take a blueprint option if translating empty values. Leaving translations empty can also be useful 505 | //&& !V::empty($layouts[$layoutIndex]['columns'][$columnIndex]['blocks'][$blockIndex]['content']) 506 | ){ 507 | //dump('Got a translation !='.$block['type'].'/'.$fieldName); 508 | 509 | // Replace the default lang block content with the translated one. 510 | $block['content'][$fieldName]=$flattenedLayouts['blocks'][$blockID]['content'][$fieldName]; 511 | } 512 | // Todo : Handle nested fields in a field ? ? ? (structure, etc...) 513 | 514 | // Todo: the fields loop can be heavy to loop, maybe unset the field once used, to speed up the next iterations ? 515 | } 516 | // Alternative way, kirby's way, but needs to ensure that keys of the translation are not set, which requires modifying the values on save ideally, but also sanitization here. (todo) 517 | //$defaultLangLayouts[$layoutIndex]['columns'][$columnIndex]['blocks'][$blockIndex]['content'] = array_merge($defaultLangLayouts[$layoutIndex]['columns'][$columnIndex]['blocks'][$blockIndex]['content'], $layouts[$layoutIndex]['columns'][$columnIndex]['blocks'][$blockIndex]['content']); 518 | 519 | // Fallback when a block has no fields ? Are there blocks with content AND without fields ? 520 | } 521 | 522 | } 523 | 524 | // Compute simplified blueprint to fully expanded options (like original Kirby fill() function) 525 | //$defaultLangLayouts[$layoutIndex]['columns'][$columnIndex]['blocks'] = $this->blocksToValues(array_merge($defaultLangLayouts[$layoutIndex]['columns'][$columnIndex]['blocks'], $layouts[$layoutIndex]['columns'][$columnIndex]['blocks'])); 526 | $column['blocks'] = $this->blocksToValues($column['blocks']); 527 | 528 | // lazy-update/replace ? whole blocks part ? Too buggy in case items get add/removed; only works well when data is a perfect mirror. Also, array_combine tends to be quite slow. 529 | //$layouts[$layoutIndex]['columns'][$columnIndex]['blocks'] = array_combine($defaultLangLayouts[$layoutIndex]['columns'][$columnIndex]['blocks'], array_slice($layouts[$layoutIndex]['columns'][$columnIndex]['blocks']); 530 | } 531 | 532 | } 533 | 534 | // Reset keys 535 | //$defaultLangLayouts = static::keysToIndexes($defaultLangLayouts); 536 | 537 | // Remember value 538 | $this->value = $defaultLangLayouts; 539 | $this->errors = null; 540 | return $this; 541 | } 542 | 543 | // Override the layout settings blueprint, 544 | protected function setSettings($settings = null) : void { 545 | // Changed : On default lang, use native kirby function, sure not to break. 546 | if(!$this->kirby()->multilang() || !$this->kirby()->language() || $this->kirby()->language()->isDefault()){ 547 | parent::setSettings($settings); 548 | return; 549 | } 550 | 551 | if (empty($settings) === true) { 552 | $this->settings = null; 553 | return; 554 | } 555 | 556 | $settings = Blueprint::extend($settings); 557 | $settings['icon'] = 'dashboard'; 558 | $settings['type'] = 'layout'; 559 | $settings['parent'] = $this->model(); 560 | 561 | // Lines below were added compared to native function 562 | $settings = $this->adaptFieldsetToTranslation($settings); 563 | //$settings['disabled'] = true; 564 | //$settings['editable'] = false; // Adding this line disables saving of attrs/settings ? 565 | 566 | $this->settings = Fieldset::factory($settings); 567 | } 568 | 569 | // Checkme: Need to disble any validations ? 570 | // public function validations(): array { 571 | // return []; 572 | // } 573 | 574 | // Try to override these ModelWithContent methods 575 | //public function translation(string $languageCode = null) { return $this->parent->translation($languageCode); } 576 | //public function translations(); 577 | 578 | // Check if this function is called ? 579 | // protected function i18n($param = null): ?string 580 | // { 581 | // return empty($param) === false ? I18n::translate($param, $param) : null; 582 | // } 583 | } 584 | -------------------------------------------------------------------------------- /src/classes/TranslatedLayoutFieldContent.php: -------------------------------------------------------------------------------- 1 | toBlocks()->fields()->first()->translation('en') instead of current kirby style : 5 | // $field->->translation('en')->toBlocks()->fields()->first() ? 6 | // 7 | // Result : (from my understanding) 8 | // Kirby contant/translations/model management doesn't seem to really bubble as I thought they would, they are more like references to the main root parent passed down to children. 9 | // So they mostly communicate from sub-sub-child to root-parent, but not from parent-to-child-to-child 10 | 11 | use \Kirby\Cms\ModelWithContent; 12 | use \Kirby\Exception\LogicException; 13 | 14 | // Class for extending the default layout field to have translateable content with layout structure sync 15 | class TranslatedLayoutFieldContent extends ModelWithContent { 16 | const CLASS_ALIAS = 'TranslatedLayoutFieldContent'; 17 | 18 | private ?ModelWithContent $parent = NULL; 19 | 20 | public function __construct(?ModelWithContent $parent){ 21 | if(!$parent) throw new LogicException("TranslatedLayoutFieldContent needs a parent !"); 22 | $this->parent = $parent; 23 | } 24 | 25 | // Map parent abstract functions to this 26 | public function contentFileName(): string { return $parent->contentFileName(); } 27 | public function permissions() { return $parent->permissions(); } 28 | public function root(): ?string { return $parent->root(); } 29 | public function blueprint() { return $parent->blueprint(); } 30 | public function panel() { return $parent->panel(); } 31 | protected function commit(string $action, array $arguments, Closure $callback) { return $parent->commit($action, $arguments, $callback); } 32 | 33 | // Map any other functions ? 34 | public function blueprints(string $inSection = null): array { return $this->parent->blueprints($inSection); } 35 | public function content(string $languageCode = null) { 36 | return $this->parent->content($languageCode); 37 | } 38 | public function contentFile(string $languageCode = null, bool $force = false): string { return $this->parent->contentFile($languageCode, $force); } 39 | public function contentFiles(): array { return $this->parent->contentFiles(); } 40 | public function contentFileData(array $data, string $languageCode = null): array { return $this->parent->contentFileData($data, $languageCode); } 41 | public function contentFileDirectory(): ?string { return $this->parent->contentFileDirectory(); } 42 | public function contentFileExtension(): string { return $this->parent->contentFileExtension(); } 43 | public function decrement(string $field, int $by = 1, int $min = 0) { return $this->parent->decrement($field, $by, $min); } 44 | public function errors(): array { return $this->parent->errors(); } 45 | public function increment(string $field, int $by = 1, int $max = null) { return $this->parent->increment(); } 46 | public function isLocked(): bool { return $this->parent->isLocked(); } 47 | public function isValid(): bool { return $this->parent->isValid(); } 48 | public function lock() { return $this->parent->lock(); } 49 | public function query(string $query = null, string $expect = null) { return $this->parent->query($query, $expect); } 50 | public function readContent(string $languageCode = null): array { return $this->parent->readContent($languageCode); } 51 | public function save(array $data = null, string $languageCode = null, bool $overwrite = false) { 52 | return $this->parent->save($data, $languageCode, $overwrite); } 53 | protected function saveContent(array $data = null, bool $overwrite = false) { return $this->parent->saveContent($data, $overwrite); } 54 | protected function saveTranslation(array $data = null, string $languageCode = null, bool $overwrite = false) { return $this->parent->saveTranslation($data, $languageCode, $overwrite); } 55 | protected function setContent(array $content = null) { return $this->parent->setContent($content); } 56 | protected function setTranslations(array $translations = null) { return $this->parent->setTranslations($translations); } 57 | public function toSafeString(string $template = null, array $data = [], string $fallback = ''): string { return $this->parent->toSafeString($template, $data, $fallback); } 58 | public function toString(string $template = null, array $data = [], string $fallback = '', string $handler = 'template'): string { return $this->parent->toString($template, $data, $fallback, $handler); } 59 | public function translation(string $languageCode = null) { return $this->parent->translation($languageCode); } 60 | public function translations() { return $this->parent->translations(); } 61 | public function update(array $input = null, string $languageCode = null, bool $validate = false) { return $this->parent->update($input, $languageCode, $validate); } 62 | public function writeContent(array $data, string $languageCode = null): bool { return $this->parent->writeContent($data, $languageCode); } 63 | public function panelIcon(array $params = null): ?array { return $this->parent->panelIcon($params); } 64 | public function panelImage($settings = null): ?array { return $this->parent->panelImage($settings); } 65 | public function panelOptions(array $unlock = []): array { return $this->parent->panelOptions($unlock); } 66 | } 67 | -------------------------------------------------------------------------------- /src/classes/TranslatedLayoutHelpers.php: -------------------------------------------------------------------------------- 1 | exists() ) throw new InvalidArgumentException('fieldBlueprint() only works on existing fields !'); 12 | $key = $field->key(); 13 | if(!$key || $key==='title' ) throw new InvalidArgumentException('Sorry, Kirby provides no FormField nor FieldBlueprint for the title !'); // Todo: provide a fallback ? 14 | 15 | $page = $field->model(); 16 | if($page instanceof \Kirby\Cms\Page === true || $page instanceof \Kirby\Cms\Site === true){ 17 | return getFieldBlueprint($page, $key, $returnParsed); 18 | } 19 | 20 | // Should this rather return null ? 21 | throw new InvalidArgumentException("The provided field has no valid model !"); 22 | return null; 23 | }; 24 | 25 | // Helper for retrieving blueprint info from a Content or Cms object 26 | function getFieldBlueprint(\Kirby\Cms\Page | \Kirby\Cms\Site $page, string $key, bool $returnParsed = false) : ?array { 27 | $pageBlueprint = $page->blueprint(); 28 | $fieldBlueprint = $pageBlueprint->field($key); 29 | // SiteBlueprint has no title field... try calling the method directly ? 30 | if(!$fieldBlueprint) $fieldBlueprint = $pageBlueprint->{$key}(); 31 | 32 | if(!$fieldBlueprint || !is_array($fieldBlueprint) ) throw new InvalidArgumentException('Weirdly, the field "'.$key.'" doesn\'t exist in the blueprint !'); 33 | 34 | if($returnParsed) return \Kirby\Cms\Blueprint::fieldProps($fieldBlueprint??[]); 35 | return $fieldBlueprint??null; 36 | }; 37 | 38 | // Syncs 2 data structures, returning fully translated data 39 | // Mainly for frontend usage 40 | function syncLanguages(array $defaultLangLayouts, array $translationData, \Kirby\Cms\Fieldsets $fieldsets, \Kirby\Cms\Fieldset|null $attrsFieldset/*, ?\Kirby\Content\Field $field=null*/) : array { 41 | // Got valid translation data ? 42 | if( 43 | // $translationData && is_array($translationData) && // Got data ? 44 | array_key_exists(TranslatedLayoutField::BLOCKS_KEY, $translationData) && array_key_exists(TranslatedLayoutField::LAYOUTS_KEY, $translationData) || // Got expected columns ? 45 | !is_array($translationData[TranslatedLayoutField::BLOCKS_KEY]) && !is_array($translationData[TranslatedLayoutField::LAYOUTS_KEY]) // Are they arrays ? 46 | ){ 47 | // Inject translations 48 | 49 | // Loop the default language's structure and let translation content replace it 50 | foreach ($defaultLangLayouts as $layoutIndex => &$layout) { // <-- Apply blockstovalues 51 | $layoutID = $layout['id']??$layoutIndex; 52 | 53 | // Check the layout settings / attrs 54 | if ( $attrsFieldset !== null && array_key_exists($layoutID, $translationData['layouts']) && array_key_exists('attrs', $translationData['layouts'][$layoutID]) ) { // 55 | 56 | // Check for translations 57 | $attrFields = $attrsFieldset->fields(); 58 | if( count($attrFields) > 0 ){ 59 | 60 | // Loop default attrs by field 61 | foreach($attrFields as $fieldName => $attrField){ 62 | // Translate if needed 63 | if( 64 | array_key_exists('translate', $attrField) && $attrField['translate'] === true // the field translates 65 | && isset($translationData[TranslatedLayoutField::LAYOUTS_KEY][$layoutID]['attrs'][$fieldName]) // The translation exists 66 | && !V::empty($translationData[TranslatedLayoutField::LAYOUTS_KEY][$layoutID]['attrs'][$fieldName])// The translation is not empty 67 | ){ 68 | $layout['attrs'][$fieldName] = $translationData[TranslatedLayoutField::LAYOUTS_KEY][$layoutID]['attrs'][$fieldName]; 69 | // Todo : What if translation is empty ? 70 | // !V::empty($layouts[$layoutIndex]['attrs'][$attrIndex]) 71 | 72 | // Todo: also handle nested fields translation ? 73 | } 74 | } 75 | } 76 | } 77 | 78 | // Translate columns & contents 79 | foreach($layout['columns'] as $columnIndex => &$column) { 80 | $columnID = $column['id']??$columnIndex; 81 | 82 | // Loop blocks and restrict them to the default language 83 | foreach( $column['blocks'] as $blockIndex => &$block){ 84 | $blockID = $block['id']??$blockIndex; 85 | $blockType = $block['type']; 86 | // Note: If code breaks: Useful inspiration for syncing translations --> ModelWithContent.php [in function content()] : 87 | 88 | // Ignore unknown blocks (leave untranslated) 89 | if(!array_key_exists($blockType, $fieldsets->data())){ 90 | continue; 91 | } 92 | 93 | $translateByDefault = false; // todo: parse this from a plugin option ? 94 | 95 | // Get blueprint block attributes (its translation config) 96 | if ($blockBlueprint = $fieldsets->find($blockType)) { 97 | // Don't translate the whole block ! 98 | if(!$blockBlueprint->translate()){ 99 | continue; 100 | } 101 | 102 | $blockFields = $blockBlueprint->fields(); 103 | foreach($blockFields as $fieldKey => &$field){ 104 | 105 | $doTranslateThis = $translateByDefault; 106 | if(array_key_exists('translate', $field)){ 107 | $doTranslateThis = $field['translate']===true; 108 | } 109 | // Need to translate ? 110 | if($doTranslateThis){ 111 | // Translation available ? 112 | if( 113 | array_key_exists($blockID, $translationData[TranslatedLayoutField::BLOCKS_KEY]) && 114 | array_key_exists($fieldKey, $translationData[TranslatedLayoutField::BLOCKS_KEY][$blockID]['content']) && 115 | !V::empty($translationData[TranslatedLayoutField::BLOCKS_KEY][$blockID]['content'][$fieldKey]) // The translation is not empty 116 | ){ 117 | // Replace the default lang block content with the translated one. 118 | $block['content'][$fieldKey] = $translationData[TranslatedLayoutField::BLOCKS_KEY][$blockID]['content'][$fieldKey]; 119 | 120 | // Todo : Handle nested fields in a field ? ? ? (structure, etc...) 121 | } 122 | else { 123 | // No translation available, keep default lang as fallback 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | // Return the default or translated content. 134 | return $defaultLangLayouts; 135 | } 136 | 137 | ?> -------------------------------------------------------------------------------- /src/components/TranslatedBlock.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 130 | 131 | 134 | -------------------------------------------------------------------------------- /src/components/TranslatedBlocks.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 149 | 150 | 153 | -------------------------------------------------------------------------------- /src/components/TranslatedBlocksField.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 94 | 95 | 108 | -------------------------------------------------------------------------------- /src/components/TranslatedLayout.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 71 | 72 | 75 | -------------------------------------------------------------------------------- /src/components/TranslatedLayoutField.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 97 | 98 | 127 | -------------------------------------------------------------------------------- /src/components/TranslatedLayoutHelpers.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | 4 | } -------------------------------------------------------------------------------- /src/components/TranslatedLayoutMixin.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | computed: { 4 | 5 | // Editing is only allowed in the default language 6 | layoutEditingIsDisabled() { 7 | // Note: on single lang installations, $language is null --> always allow editing layouts 8 | //debugger; 9 | if(!this.$panel.language) return false; 10 | 11 | // Behave as normal on 12 | 13 | // Is the current language default AND are we child of translated-*-component ? 14 | return (!this.$panel.language.default) && this.isWithinTranslatedComponent; 15 | //return window.panel.$language.default; 16 | }, 17 | 18 | // Helper to figure out if a component is in / or rather / 19 | isWithinTranslatedComponent(){ 20 | let tmpParent = this; // Start with self 21 | const translatedComponents = ['translatedblocks', 'translatedlayout']; 22 | while( tmpParent != this.$root && tmpParent != null && tmpParent!=null ){ 23 | if( tmpParent.type && translatedComponents.includes(tmpParent.type) ){ 24 | return true; 25 | } 26 | tmpParent = tmpParent.$parent; 27 | } 28 | return false; 29 | }, 30 | }, 31 | methods: { 32 | // Helper for replacing native methods on mount. 33 | // Before: Native functions : myFunc, Custom functions : myFuncCustom. 34 | // After : Native functions : myFuncNative, Custom functions : myFuncCustom & myFunc 35 | // So we replace the native functions, still being able to call them. 36 | invertCustomAndNativeFunctions(funcNames){ 37 | for(const fn of funcNames){ 38 | if(true){ // Todo: make debug only ? 39 | if( !this[fn] ){ // original doesn't exist ! 40 | window.console.log("Native function replacement hack: `"+fn+"` doesn't exist anymore. Please fix me."); 41 | continue; 42 | } 43 | if( !this[fn + 'Custom'] ){ // Target 44 | window.console.log("Native function replacement hack: `"+fn+"Custom` doesn't exist. Please implement it !"); 45 | continue; 46 | } 47 | } 48 | if(this[fn + 'Native']) continue; // if Native is set, this has already been bound 49 | this[fn + 'Native'] = this[fn]; this[fn] = this[fn + 'Custom']; 50 | } 51 | } 52 | }, 53 | } -------------------------------------------------------------------------------- /src/components/TranslatedLayouts.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 182 | 183 | 186 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import TranslatedLayoutField from "~/components/TranslatedLayoutField.vue"; 3 | import TranslatedBlocksField from "~/components/TranslatedBlocksField.vue"; 4 | import TranslatedBlocks from "~/components/TranslatedBlocks.vue"; 5 | import TranslatedBlock from "~/components/TranslatedBlock.vue"; 6 | //import TranslatedLayout from "~/components/TranslatedLayouterLayout.vue" 7 | 8 | panel.plugin("daandelange/translatedlayout", { 9 | components: { 10 | //'k-layout' : TranslatedLayout, // Not possible, locally-registered component, not globally 11 | // Important note: There's some kind of recursion replacing the plugin that loads another replaced plugin. First over-ride the child, then the parent container ! 12 | 'k-block' : TranslatedBlock, // Override globally registered k-block 13 | 'k-blocks' : TranslatedBlocks,// Override globally registered k-blocks 14 | 15 | }, 16 | 17 | fields: { 18 | translatedlayout: TranslatedLayoutField, 19 | //translatedlayout : { 20 | // extends: 'k-layout-field', // <-- works fine without extending anything on js side 21 | //} 22 | translatedblocks: TranslatedBlocksField, 23 | // translatedblocks: { 24 | // extends: 'k-blocks-field', // <-- works fine without extending anything on js side 25 | // }, 26 | }, 27 | }); 28 | --------------------------------------------------------------------------------