├── .gitignore ├── README.md ├── composer.json ├── config └── nova-translation-manager.php ├── dist ├── css │ └── tool.css ├── js │ └── tool.js └── mix-manifest.json ├── package.json ├── resources ├── js │ ├── components │ │ └── Tool.vue │ └── tool.js ├── sass │ └── tool.scss └── views │ └── navigation.blade.php ├── routes └── api.php ├── src ├── Console │ └── InstallCommand.php ├── Helpers │ └── TranslationHelper.php ├── Http │ ├── Controllers │ │ ├── GroupsController.php │ │ ├── LocalesController.php │ │ ├── TranslationsController.php │ │ └── UploadController.php │ ├── Middleware │ │ └── Authorize.php │ └── Requests │ │ └── UploadRequest.php ├── LaravelNovaTranslationsManager.php ├── Models │ └── Translation.php └── ToolServiceProvider.php └── webpack.mix.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /node_modules 4 | package-lock.json 5 | composer.phar 6 | composer.lock 7 | phpunit.xml 8 | .phpunit.result.cache 9 | .DS_Store 10 | Thumbs.db 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Nova Translation Manager 2 | 3 | This tool is build in order to maintain translations via Laravel Nova while using the [Laravel Translation Manager] by [Barry van den Heuvel]. It also provides an integration with the [Laravel Vue I18N Generator] package from [Martin Lindhe]. The Laravel Vue i18n Generator package is optional and should be installed manually. Please see the [Laravel Vue i18n Generator Docs] for more information. 4 | 5 | ## Installation 6 | 7 | ``` 8 | composer require voicecode/laravel-nova-translation-manager 9 | ``` 10 | 11 | Then, publish the config file and make sure you set the correct value for using the Vue i18n package. 12 | 13 | ``` 14 | php artisan vendor:publish --provider="Voicecode\LaravelNovaTranslationsManager\ToolServiceProvider" 15 | ``` 16 | 17 | While this tool is build upon Laravel Translation Manager, this will be installed automatically when installing this tool. Make sure to publish the files provided by Translation Manager and run the migrations. 18 | 19 | ``` 20 | php artisan vendor:publish --provider="Barryvdh\TranslationManager\ManagerServiceProvider" --tag=migrations 21 | php artisan migrate 22 | ``` 23 | 24 | Now in NovaServiceProvider, make sure you register this tool in the tools method. 25 | 26 | ``` 27 | use Voicecode\LaravelNovaTranslationsManager\LaravelNovaTranslationsManager; 28 | 29 | /** 30 | * Get the tools that should be listed in the Nova sidebar. 31 | * 32 | * @return array 33 | */ 34 | public function tools() 35 | { 36 | return [ 37 | new LaravelNovaTranslationsManager(), 38 | ]; 39 | } 40 | ``` 41 | 42 | ## Supported locales 43 | 44 | When starting off with an empty database, it's mandatory to create a first database record. You can do this by running the install command. This will generate a record based on your current app locale. 45 | 46 | ``` 47 | php artisan translation-manager:install 48 | ``` 49 | 50 | ## Translating this package 51 | 52 | You can easily translate all texts of this package in the Laravel Nova JSON translation file. I'd be happy to add translations for your language. An example can be found in the translations folder. 53 | 54 | ## Note for Vue i18n Generator users 55 | 56 | The package ships with a config file where you can set the value of output messages. Make sure this is set to false, otherwise Laravel Nova will throw some errors while publishing translation files. (Thanks [Martin Lindhe] for merging this PR :-)) 57 | 58 | ``` 59 | 'showOutputMessages' => false, 60 | ``` 61 | 62 | ## About Voicecode B.V. 63 | Voicecode B.V. 64 | Faradaystraat 11 65 | 2014 EN Haarlem 66 | The Netherlands 67 | 68 | [Laravel Translation Manager]: https://github.com/barryvdh/laravel-translation-manager 69 | [Laravel Vue I18N Generator]: https://github.com/martinlindhe/laravel-vue-i18n-generator 70 | [Laravel Vue i18n Generator Docs]: https://github.com/martinlindhe/laravel-vue-i18n-generator/blob/master/README.md 71 | [Martin Lindhe]: https://github.com/martinlindhe 72 | [Barry van den Heuvel]: https://github.com/barryvdh -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voicecode/laravel-nova-translations-manager", 3 | "description": "A translation management tool for Laravel Nova.", 4 | "keywords": [ 5 | "laravel", 6 | "nova", 7 | "translations", 8 | "translation", 9 | "translation manager", 10 | "nova" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Voicecode B.V.", 16 | "email": "info@voicecode.nl" 17 | } 18 | ], 19 | "require": { 20 | "barryvdh/laravel-translation-manager": "^0.6", 21 | "php": ">=7.1.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Voicecode\\LaravelNovaTranslationsManager\\": "src/" 26 | } 27 | }, 28 | "extra": { 29 | "laravel": { 30 | "providers": [ 31 | "Voicecode\\LaravelNovaTranslationsManager\\ToolServiceProvider" 32 | ] 33 | } 34 | }, 35 | "config": { 36 | "sort-packages": true 37 | }, 38 | "minimum-stability": "dev", 39 | "prefer-stable": true 40 | } 41 | -------------------------------------------------------------------------------- /config/nova-translation-manager.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'active' => false, 11 | 'umd' => true, 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /dist/css/tool.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-monkeys-bv/laravel-nova-translation-manager/b160663b6669990af0e55473f75fc8b461f564ca/dist/css/tool.css -------------------------------------------------------------------------------- /dist/js/tool.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function s(a){if(e[a])return e[a].exports;var n=e[a]={i:a,l:!1,exports:{}};return t[a].call(n.exports,n,n.exports,s),n.l=!0,n.exports}s.m=t,s.c=e,s.d=function(t,e,a){s.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:a})},s.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return s.d(e,"a",e),e},s.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},s.p="",s(s.s=0)}([function(t,e,s){s(1),t.exports=s(6)},function(t,e,s){Nova.booting(function(t,e,a){t.config.devtools=!0,e.addRoutes([{name:"laravel-nova-translations-manager",path:"/laravel-nova-translations-manager",component:s(2)}])})},function(t,e,s){var a=s(3)(s(4),s(5),!1,null,null,null);t.exports=a.exports},function(t,e){t.exports=function(t,e,s,a,n,l){var o,i=t=t||{},r=typeof t.default;"object"!==r&&"function"!==r||(o=t,i=t.default);var c,u="function"==typeof i?i.options:i;if(e&&(u.render=e.render,u.staticRenderFns=e.staticRenderFns,u._compiled=!0),s&&(u.functional=!0),n&&(u._scopeId=n),l?(c=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),a&&a.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(l)},u._ssrRegister=c):a&&(c=a),c){var d=u.functional,p=d?u.render:u.beforeCreate;d?(u._injectStyles=c,u.render=function(t,e){return c.call(e),p(t,e)}):u.beforeCreate=p?[].concat(p,c):[c]}return{esModule:o,exports:i,options:u}}},function(t,e,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default={metaInfo:function(){return{title:"Translations Manager"}},computed:{groupSelected:function(){return null!==this.group}},data:function(){return{file:null,group:null,groups:[],newGroup:null,selectedGroup:null,importType:null,keywords:null,updatedKeyword:null,selectedKeyword:null,locales:[],newLocale:null,defaultLocale:null,selectedLocale:null,selected:{},translations:[],createModalOpened:!1,updateModalOpened:!1,deleteModalOpened:!1,deleteGroupModalOpened:!1,deleteLocaleModalOpened:!1,updateKeywordModalOpened:!1,apiUrl:"/nova-vendor/laravel-nova-translations-manager/",publishAfterUpload:!0}},mounted:function(){this.getGroups(),this.getLocales()},methods:{onFileChange:function(t){this.file=t.target.files[0]},uploadCSV:function(t){var e=this,s=new FormData;s.append("file",this.file),s.append("publish",this.publishAfterUpload),Nova.request().post(this.apiUrl+"upload",s,{headers:{"content-type":"multipart/form-data"}}).then(function(t){e.$toasted.show("The file has been processed!",{type:"success"})}).catch(function(t){e.$toasted.show("Something went wrong here...",{type:"error"})}),this.file=null},getGroups:function(){var t=this;Nova.request().get(this.apiUrl+"translations").then(function(e){t.groups=e.data})},getLocales:function(){var t=this;axios.get(this.apiUrl+"locales").then(function(e){t.locales=e.data.locales,t.defaultLocale=e.data.defaultLocale})},setGroup:function(){var t=this;axios.get(this.apiUrl+"translations/"+this.group).then(function(e){t.translations=e.data})},createGroup:function(){var t=this;if(null!==this.newGroup&&""!==this.newGroup){var e={};e.group=this.newGroup,axios.post(this.apiUrl+"groups",e).then(function(e){t.group=e.data.group,t.getGroups(),t.setGroup(t.group),t.newGroup=null}).catch(function(e){t.$toasted.show(e.response.data.errors.group[0],{type:"error"})})}},deleteGroup:function(){var t=this;null!==this.group&&""!==this.group&&axios.delete(this.apiUrl+"groups/"+this.group).then(function(e){t.$toasted.show("The group has been deleted!",{type:"success"}),t.getGroups(),t.group=null,t.deleteGroupModalOpened=!1})},createKeywords:function(){var t=this,e={};e.group=this.group,e.keywords=this.keywords,axios.post(this.apiUrl+"translations",e).then(function(e){t.closeCreateModal(),t.setGroup(t.group),t.$toasted.show("The translation has been created!",{type:"success"})})},updateKeyword:function(){var t=this;if(null!==this.selectedGroup&&""!==this.selectedGroup&&null!==this.selectedKeyword&&""!==this.selectedKeyword&&null!==this.updatedKeyword&&""!==this.updatedKeyword){var e={};e.group=this.selectedGroup,e.new_key=this.updatedKeyword,e.old_key=this.selectedKeyword,axios.put(this.apiUrl+"translations/key",e).then(function(e){t.setGroup(t.group),t.closeUpdateKeywordModal(),t.$toasted.show("The keyword has been updated!",{type:"success"})})}},deleteKeyword:function(){var t=this;null!==this.selectedGroup&&""!==this.selectedGroup&&null!==this.selectedKeyword&&""!==this.selectedKeyword&&axios.delete(this.apiUrl+"translations/"+this.selectedGroup+"/"+this.selectedKeyword).then(function(e){t.setGroup(t.group),t.closeDeleteModal(),t.$toasted.show("The translation has been updated!",{type:"success"})})},updateTranslation:function(){var t=this,e={};e.id=this.selected.id,e.value=this.selected.value,axios.put(this.apiUrl+"translations/"+this.selected.id,e).then(function(e){t.closeUpdateModal(),t.setGroup(t.group),t.$toasted.show("The translation has been updated!",{type:"success"})})},exportTranslations:function(){var t=this,e={};e.group=this.group,axios.post(this.apiUrl+"translations/export",e).then(function(e){t.$toasted.show("The translations have been exported!",{type:"success"}),t.setGroup()})},exportAllTranslations:function(){var t=this,e={group:"*"};axios.post(this.apiUrl+"translations/export",e).then(function(e){t.$toasted.show("The translations have been exported!",{type:"success"})})},importTranslations:function(){var t=this;if(null!==this.importType&&""!==this.importType){var e={};e.type=this.importType,axios.post(this.apiUrl+"translations/import",e).then(function(e){t.$toasted.show("The translations have been imported!",{type:"success"})})}},fixMissingTranslation:function(t){var e=this,s=t[Object.keys(t)[0]],a={};a.key=s.key,a.group=s.group,axios.post(this.apiUrl+"translations/fix",a).then(function(t){e.setGroup(e.group),e.$toasted.show("The translation has been fixed!",{type:"success"})})},fixMissingTranslations:function(){var t=this,e={};e.group=this.group,axios.post(this.apiUrl+"translations/fix/group",e).then(function(e){t.setGroup(t.group),t.$toasted.show("All translations within this group have been fixed!",{type:"success"})})},createLocale:function(){var t=this;if(null!==this.newLocale&&""!==this.newLocale){var e={};e.locale=this.newLocale,axios.post(this.apiUrl+"locales",e).then(function(e){t.getLocales(),t.newLocale=null,t.$toasted.show("The locale has been created!",{type:"success"})}).catch(function(e){t.$toasted.show(e.response.data.errors.locale[0],{type:"error"})})}},deleteLocale:function(){var t=this;null!==this.selectedLocale&&""!==this.selectedLocale&&axios.delete(this.apiUrl+"locales/"+this.selectedLocale).then(function(e){t.closeDeleteLocaleModal(),t.setGroup(t.group),t.getLocales(),t.$toasted.show("The locale has been deleted!",{type:"success"})})},openCreateModal:function(){this.createModalOpened=!0},closeCreateModal:function(){this.keywords=null,this.createModalOpened=!1},openUpdateModal:function(t){this.selected=Object.assign({},t),this.updateModalOpened=!0},closeUpdateModal:function(){this.selected=Object.assign({},{}),this.updateModalOpened=!1},openDeleteGroupModal:function(){this.deleteGroupModalOpened=!0},closeDeleteGroupModal:function(){this.deleteGroupModalOpened=!1},openDeleteModal:function(t){this.selectedGroup=this.group,this.selectedKeyword=t,this.deleteModalOpened=!0},closeDeleteModal:function(){this.selectedGroup=null,this.selectedKeyword=null,this.deleteModalOpened=!1},openUpdateKeywordModal:function(t){this.selectedGroup=this.group,this.updatedKeyword=t,this.selectedKeyword=t,this.updateKeywordModalOpened=!0},closeUpdateKeywordModal:function(){this.selectedKeyword=null,this.updatedKeyword=null,this.updateKeywordModalOpened=!1},openDeleteLocaleModal:function(){this.deleteLocaleModalOpened=!0},closeDeleteLocaleModal:function(){this.deleteLocaleModalOpened=!1}}}},function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",[s("heading",{staticClass:"mb-6"},[t._v(t._s(t.__("Translation Manager")))]),t._v(" "),s("div",{staticClass:"flex flex-wrap -mx-2"},[s("div",{staticClass:"w-1/2 px-2"},[s("card",{staticClass:"p-6"},[s("h3",{staticClass:"mb-4"},[t._v(t._s(t.__("Import Translations")))]),t._v(" "),s("div",{staticClass:"flex items-end"},[s("div",{staticClass:"w-2/3"},[s("select",{directives:[{name:"model",rawName:"v-model",value:t.importType,expression:"importType"}],staticClass:"form-control form-input form-input-bordered w-full",attrs:{size:"1"},on:{change:function(e){var s=Array.prototype.filter.call(e.target.options,function(t){return t.selected}).map(function(t){return"_value"in t?t._value:t.value});t.importType=e.target.multiple?s:s[0]}}},[s("option",{attrs:{value:"replace"}},[t._v(t._s(t.__("Replace")))]),t._v(" "),s("option",{attrs:{value:"append"}},[t._v(t._s(t.__("Append")))])])]),t._v(" "),s("div",{staticClass:"w-1/3 px-2"},[s("button",{staticClass:"btn btn-default btn-primary w-full",on:{click:t.importTranslations}},[t._v("\n "+t._s(t.__("Import"))+"\n ")])])])])],1),t._v(" "),s("div",{staticClass:"w-1/2 px-2"},[s("card",{staticClass:"p-6"},[s("h3",{staticClass:"mb-4"},[t._v(t._s(t.__("Select Group")))]),t._v(" "),s("div",{staticClass:"flex items-end"},[s("div",{staticClass:"w-2/3"},[s("select",{directives:[{name:"model",rawName:"v-model",value:t.group,expression:"group"}],staticClass:"form-control form-input form-input-bordered w-full",attrs:{size:"1"},on:{change:[function(e){var s=Array.prototype.filter.call(e.target.options,function(t){return t.selected}).map(function(t){return"_value"in t?t._value:t.value});t.group=e.target.multiple?s:s[0]},t.setGroup]}},t._l(t.groups,function(e,a){return s("option",{domProps:{value:e}},[t._v(t._s(e))])}),0)])])])],1),t._v(" "),s("div",{staticClass:"w-1/2 px-2 mt-6"},[s("card",{staticClass:"p-6"},[s("h3",{staticClass:"mb-4"},[t._v(t._s(t.__("Create Group")))]),t._v(" "),s("div",{staticClass:"flex items-end"},[s("div",{staticClass:"w-2/3"},[s("input",{directives:[{name:"model",rawName:"v-model",value:t.newGroup,expression:"newGroup"}],staticClass:"form-control form-input form-input-bordered w-full",domProps:{value:t.newGroup},on:{input:function(e){e.target.composing||(t.newGroup=e.target.value)}}})]),t._v(" "),s("div",{staticClass:"w-1/3 px-2"},[s("button",{staticClass:"btn btn-default btn-primary w-full",on:{click:t.createGroup}},[t._v("\n "+t._s(t.__("Create"))+"\n ")])])])])],1),t._v(" "),s("div",{staticClass:"w-1/2 px-2 mt-6"},[s("card",{staticClass:"p-6"},[s("h3",{staticClass:"mb-4"},[t._v(t._s(t.__("Delete Locale")))]),t._v(" "),s("div",{staticClass:"flex"},[s("div",{staticClass:"w-2/3"},[s("select",{directives:[{name:"model",rawName:"v-model",value:t.selectedLocale,expression:"selectedLocale"}],staticClass:"form-control form-input form-input-bordered w-full",attrs:{size:"1"},on:{change:function(e){var s=Array.prototype.filter.call(e.target.options,function(t){return t.selected}).map(function(t){return"_value"in t?t._value:t.value});t.selectedLocale=e.target.multiple?s:s[0]}}},t._l(t.locales,function(e,a){return s("option",{domProps:{value:e}},[t._v(t._s(e))])}),0)]),t._v(" "),s("div",{staticClass:"w-1/3 px-2"},[s("button",{staticClass:"btn btn-default btn-danger w-full",on:{click:t.openDeleteLocaleModal}},[t._v("\n "+t._s(t.__("Delete"))+"\n ")])])])])],1),t._v(" "),s("div",{staticClass:"w-1/2 px-2 mt-6"},[s("card",{staticClass:"p-6"},[s("h3",{staticClass:"mb-4"},[t._v(t._s(t.__("Create Locale")))]),t._v(" "),s("div",{staticClass:"flex items-end"},[s("div",{staticClass:"w-2/3"},[s("input",{directives:[{name:"model",rawName:"v-model",value:t.newLocale,expression:"newLocale"}],staticClass:"form-control form-input form-input-bordered w-full",domProps:{value:t.newLocale},on:{input:function(e){e.target.composing||(t.newLocale=e.target.value)}}})]),t._v(" "),s("div",{staticClass:"w-1/3 px-2"},[s("button",{staticClass:"btn btn-default btn-primary w-full",on:{click:t.createLocale}},[t._v("\n "+t._s(t.__("Create"))+"\n ")])])])])],1),t._v(" "),s("div",{staticClass:"w-1/2 px-2 mt-6"},[s("card",{staticClass:"p-6"},[s("h3",{staticClass:"mb-4"},[t._v(t._s(t.__("Upload CSV")))]),t._v(" "),s("div",{staticClass:"flex items-end mb-4"},[s("div",{staticClass:"w-2/3"},[s("input",{staticClass:"form-control form-input form-input-bordered w-full",attrs:{type:"file"},on:{change:t.onFileChange}})]),t._v(" "),s("div",{staticClass:"w-1/3 px-2"},[s("button",{staticClass:"btn btn-default btn-primary w-full",on:{click:t.uploadCSV}},[t._v("\n "+t._s(t.__("Upload"))+"\n ")])])]),t._v(" "),s("label",{attrs:{for:"publish-after-upload"}},[s("input",{directives:[{name:"model",rawName:"v-model",value:t.publishAfterUpload,expression:"publishAfterUpload"}],attrs:{type:"checkbox",id:"publish-after-upload",name:"publish-after-upload","true-value":!0,"false-value":!1},domProps:{checked:Array.isArray(t.publishAfterUpload)?t._i(t.publishAfterUpload,null)>-1:t.publishAfterUpload},on:{change:function(e){var s=t.publishAfterUpload,a=e.target,n=!!a.checked;if(Array.isArray(s)){var l=t._i(s,null);a.checked?l<0&&(t.publishAfterUpload=s.concat([null])):l>-1&&(t.publishAfterUpload=s.slice(0,l).concat(s.slice(l+1)))}else t.publishAfterUpload=n}}}),t._v(" \n "+t._s(t.__("Publish after upload"))+"\n ")])])],1)]),t._v(" "),t.groupSelected?s("div",{staticClass:"mt-6"},[t.groupSelected?s("button",{staticClass:"btn btn-default btn-primary mr-3",on:{click:t.openCreateModal}},[t._v("\n "+t._s(t.__("Add Keyword"))+"\n ")]):t._e(),t._v(" "),t.groupSelected?s("button",{staticClass:"btn btn-default btn-primary mr-3",on:{click:t.exportTranslations}},[t._v("\n "+t._s(t.__("Publish"))+"\n ")]):t._e(),t._v(" "),t.groupSelected?s("button",{staticClass:"btn btn-default btn-primary mr-3",on:{click:t.fixMissingTranslations}},[t._v("\n "+t._s(t.__("Fix Translations"))+"\n ")]):t._e(),t._v(" "),t.groupSelected?s("button",{staticClass:"btn btn-default btn-danger",on:{click:t.openDeleteGroupModal}},[t._v("\n "+t._s(t.__("Delete Group"))+"\n ")]):t._e()]):t._e(),t._v(" "),t.groupSelected?s("card",{staticClass:"mt-6"},[s("table",{staticClass:"table w-full"},[s("thead",[s("th",{staticClass:"text-left"},[t._v("Keyword")]),t._v(" "),t._l(t.locales,function(e,a){return s("th",{staticClass:"text-left"},[t._v(t._s(e))])}),t._v(" "),s("th",{staticClass:"text-right"})],2),t._v(" "),s("tbody",t._l(t.translations,function(e){return e[t.defaultLocale]?s("tr",[s("td",[s("span",{staticClass:"cursor-pointer",on:{click:function(s){return t.openUpdateKeywordModal(e[t.defaultLocale].key)}}},[t._v("\n "+t._s(e[t.defaultLocale].key)+"\n ")])]),t._v(" "),t._l(t.locales,function(a,n){return e[a]?s("td",{on:{click:function(s){return t.openUpdateModal(e[a])}}},[e[a]&&null!==e[a].value?s("span",{class:["cursor-pointer",1==e[a].status?"font-bold":""]},[e[a].value.length>80?s("span",[t._v(t._s(e[a].value.substring(0,80))+"...")]):s("span",[t._v(t._s(e[a].value))])]):s("span",[s("em",{staticClass:"text-danger"},[t._v(t._s(t.__("Not Available")))])])]):s("td",[s("button",{staticClass:"btn btn-default btn-icon btn-white float-right",on:{click:function(s){return t.fixMissingTranslation(e)}}},[t._v("\n "+t._s(t.__("Fix Translation"))+"\n ")])])}),t._v(" "),s("td",{staticClass:"text-right"},[s("button",{staticClass:"btn btn-default btn-icon btn-white float-right",on:{click:function(s){return t.openDeleteModal(e[t.defaultLocale].id)}}},[s("icon",{staticClass:"text-80",attrs:{type:"delete"}})],1)])],2):s("tr",[s("td",[s("span",{staticClass:"text-danger"},[t._v(t._s(t.__("This translation needs fixing")))])]),t._v(" "),t._l(t.locales,function(t,e){return s("td")}),s("td",{staticClass:"text-right"},[s("button",{staticClass:"btn btn-default btn-icon btn-white float-right",on:{click:function(s){return t.fixMissingTranslation(e)}}},[t._v("\n "+t._s(t.__("Fix Translation"))+"\n ")])])],2)}),0)])]):t._e(),t._v(" "),t.updateModalOpened?s("modal",{staticClass:"modal",attrs:{tabindex:"-1",role:"dialog"}},[s("card",{staticClass:"w-full"},[s("heading",{staticClass:"pt-8 px-8",attrs:{level:2}},[t._v(t._s(t.__("Update Translation")))]),t._v(" "),s("div",{staticClass:"p-8"},[s("textarea",{directives:[{name:"model",rawName:"v-model",value:t.selected.value,expression:"selected.value"}],staticClass:"w-full form-input form-input-bordered p-4",attrs:{rows:"6",cols:"90"},domProps:{value:t.selected.value},on:{input:function(e){e.target.composing||t.$set(t.selected,"value",e.target.value)}}})]),t._v(" "),s("div",{staticClass:"bg-30 px-6 py-3 flex"},[s("div",{staticClass:"flex items-center ml-auto"},[s("button",{staticClass:"btn text-80 font-normal h-9 px-3 mr-3 btn-link",attrs:{type:"button"},on:{click:function(e){return e.preventDefault(),t.closeUpdateModal(e)}}},[t._v("\n "+t._s(t.__("Cancel"))+"\n ")]),t._v(" "),s("button",{staticClass:"btn btn-default btn-primary",attrs:{type:"submit"},on:{click:function(e){return e.preventDefault(),t.updateTranslation(t.selected)}}},[t._v("\n "+t._s(t.__("Save"))+"\n ")])])])],1)],1):t._e(),t._v(" "),t.updateKeywordModalOpened?s("modal",{staticClass:"modal",attrs:{tabindex:"-1",role:"dialog"}},[s("card",{staticClass:"w-full"},[s("heading",{staticClass:"pt-8 px-8",attrs:{level:2}},[t._v(t._s(t.__("Update Keyword")))]),t._v(" "),s("div",{staticClass:"p-8"},[s("input",{directives:[{name:"model",rawName:"v-model",value:t.updatedKeyword,expression:"updatedKeyword"}],staticClass:"w-full form-input form-input-bordered p-4",domProps:{value:t.updatedKeyword},on:{input:function(e){e.target.composing||(t.updatedKeyword=e.target.value)}}})]),t._v(" "),s("div",{staticClass:"bg-30 px-6 py-3 flex"},[s("div",{staticClass:"flex items-center ml-auto"},[s("button",{staticClass:"btn text-80 font-normal h-9 px-3 mr-3 btn-link",attrs:{type:"button"},on:{click:function(e){return e.preventDefault(),t.closeUpdateKeywordModal(e)}}},[t._v("\n "+t._s(t.__("Cancel"))+"\n ")]),t._v(" "),s("button",{staticClass:"btn btn-default btn-primary",attrs:{type:"submit"},on:{click:function(e){return e.preventDefault(),t.updateKeyword(e)}}},[t._v("\n "+t._s(t.__("Save"))+"\n ")])])])],1)],1):t._e(),t._v(" "),t.createModalOpened?s("modal",{staticClass:"modal",attrs:{tabindex:"-1",role:"dialog"}},[s("card",{staticClass:"w-full"},[s("heading",{staticClass:"pt-8 px-8",attrs:{level:2}},[t._v(t._s(t.__("Add Keywords")))]),t._v(" "),s("div",{staticClass:"px-8 mt-3"},[s("p",[t._v(t._s(t.__("Add 1 key per line, without the group prefix")))])]),t._v(" "),s("div",{staticClass:"p-8"},[s("textarea",{directives:[{name:"model",rawName:"v-model",value:t.keywords,expression:"keywords"}],staticClass:"w-full form-input form-input-bordered p-4",attrs:{rows:"6",cols:"90"},domProps:{value:t.keywords},on:{input:function(e){e.target.composing||(t.keywords=e.target.value)}}})]),t._v(" "),s("div",{staticClass:"bg-30 px-6 py-3 flex"},[s("div",{staticClass:"flex items-center ml-auto"},[s("button",{staticClass:"btn text-80 font-normal h-9 px-3 mr-3 btn-link",attrs:{type:"button"},on:{click:function(e){return e.preventDefault(),t.closeCreateModal(e)}}},[t._v("\n "+t._s(t.__("Cancel"))+"\n ")]),t._v(" "),s("button",{staticClass:"btn btn-default btn-primary",attrs:{type:"submit"},on:{click:function(e){return e.preventDefault(),t.createKeywords(e)}}},[t._v("\n "+t._s(t.__("Save"))+"\n ")])])])],1)],1):t._e(),t._v(" "),t.deleteModalOpened?s("modal",{staticClass:"modal",attrs:{tabindex:"-1",role:"dialog"}},[s("card",{staticClass:"w-full"},[s("heading",{staticClass:"pt-8 px-8",attrs:{level:2}},[t._v(t._s(t.__("Delete This Translation")))]),t._v(" "),s("div",{staticClass:"px-8 mt-3 mb-3"},[s("p",[t._v(t._s(t.__("Are you sure?")))])]),t._v(" "),s("div",{staticClass:"bg-30 px-6 py-3 flex"},[s("div",{staticClass:"flex items-center ml-auto"},[s("button",{staticClass:"btn text-80 font-normal h-9 px-3 mr-3 btn-link",attrs:{type:"button"},on:{click:function(e){return e.preventDefault(),t.closeDeleteModal(e)}}},[t._v("\n "+t._s(t.__("Cancel"))+"\n ")]),t._v(" "),s("button",{staticClass:"btn btn-default btn-danger",attrs:{type:"submit"},on:{click:function(e){return e.preventDefault(),t.deleteKeyword(e)}}},[t._v("\n "+t._s(t.__("Delete"))+"\n ")])])])],1)],1):t._e(),t._v(" "),t.deleteGroupModalOpened?s("modal",{staticClass:"modal",attrs:{tabindex:"-1",role:"dialog"}},[s("card",{staticClass:"w-full"},[s("heading",{staticClass:"pt-8 px-8",attrs:{level:2}},[t._v(t._s(t.__("Delete This Group")))]),t._v(" "),s("div",{staticClass:"px-8 mt-3 mb-3"},[s("p",[t._v(t._s(t.__("Are you sure?")))])]),t._v(" "),s("div",{staticClass:"bg-30 px-6 py-3 flex"},[s("div",{staticClass:"flex items-center ml-auto"},[s("button",{staticClass:"btn text-80 font-normal h-9 px-3 mr-3 btn-link",attrs:{type:"button"},on:{click:function(e){return e.preventDefault(),t.closeDeleteGroupModal(e)}}},[t._v("\n "+t._s(t.__("Cancel"))+"\n ")]),t._v(" "),s("button",{staticClass:"btn btn-default btn-danger",attrs:{type:"submit"},on:{click:function(e){return e.preventDefault(),t.deleteGroup(e)}}},[t._v("\n "+t._s(t.__("Delete"))+"\n ")])])])],1)],1):t._e(),t._v(" "),t.deleteLocaleModalOpened?s("modal",{staticClass:"modal",attrs:{tabindex:"-1",role:"dialog"}},[s("card",{staticClass:"w-full"},[s("heading",{staticClass:"pt-8 px-8",attrs:{level:2}},[t._v(t._s(t.__("Delete This Locale")))]),t._v(" "),s("div",{staticClass:"px-8 mt-3 mb-3"},[s("p",[t._v(t._s(t.__("Are you sure?")))])]),t._v(" "),s("div",{staticClass:"bg-30 px-6 py-3 flex"},[s("div",{staticClass:"flex items-center ml-auto"},[s("button",{staticClass:"btn text-80 font-normal h-9 px-3 mr-3 btn-link",attrs:{type:"button"},on:{click:function(e){return e.preventDefault(),t.closeDeleteLocaleModal(e)}}},[t._v("\n "+t._s(t.__("Cancel"))+"\n ")]),t._v(" "),s("button",{staticClass:"btn btn-default btn-danger",attrs:{type:"submit"},on:{click:function(e){return e.preventDefault(),t.deleteLocale(e)}}},[t._v("\n "+t._s(t.__("Delete"))+"\n ")])])])],1)],1):t._e()],1)},staticRenderFns:[]}},function(t,e){}]); -------------------------------------------------------------------------------- /dist/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/tool.js": "/js/tool.js", 3 | "/css/tool.css": "/css/tool.css" 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "cross-env": "^5.0.0", 14 | "laravel-mix": "^1.0" 15 | }, 16 | "dependencies": { 17 | "vue": "^2.5.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/components/Tool.vue: -------------------------------------------------------------------------------- 1 | 341 | 342 | -------------------------------------------------------------------------------- /resources/js/tool.js: -------------------------------------------------------------------------------- 1 | Nova.booting((Vue, router, store) => { 2 | Vue.config.devtools = true 3 | router.addRoutes([ 4 | { 5 | name: 'laravel-nova-translations-manager', 6 | path: '/laravel-nova-translations-manager', 7 | component: require('./components/Tool'), 8 | }, 9 | ]) 10 | }) 11 | -------------------------------------------------------------------------------- /resources/sass/tool.scss: -------------------------------------------------------------------------------- 1 | // Nova Tool CSS 2 | -------------------------------------------------------------------------------- /resources/views/navigation.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 21 | 22 | {{ __('Translations') }} 23 | 24 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 6 | // Groups Routes. 7 | Route::delete('groups/{group}', 'GroupsController@destroy')->where(['group' => '[a-z0-9-]+']); 8 | Route::resource('groups', 'GroupsController'); 9 | 10 | // Locales Routes. 11 | // Route::delete('locales/{locale}', 'LocalesController@destroy')->where(['locale' => '[a-zA-Z0-9-]+']); 12 | 13 | Route::resource('locales', 'LocalesController'); 14 | 15 | // Translations Routes. 16 | Route::post('translations/export', 'TranslationsController@export'); 17 | Route::post('translations/fix/group', 'TranslationsController@fixGroup'); 18 | Route::post('translations/fix', 'TranslationsController@fix'); 19 | Route::post('translations/import', 'TranslationsController@import'); 20 | Route::put('translations/key', 'TranslationsController@updateKey'); 21 | Route::get('translations/{group}/{subgroup?}', 'TranslationsController@show'); 22 | Route::delete('translations/{group}/{key}', 'TranslationsController@destroy')->where(['group' => '[a-z0-9-]+'])->where(['key' => '[a-z0-9-]+']); 23 | Route::resource('translations', 'TranslationsController'); 24 | 25 | // Upload CSV. 26 | Route::post('upload', 'UploadController@upload'); 27 | }); 28 | -------------------------------------------------------------------------------- /src/Console/InstallCommand.php: -------------------------------------------------------------------------------- 1 | 0, 31 | 'locale' => app()->getLocale(), 32 | 'group' => 'test', 33 | 'key' => 'placeholder', 34 | 'value' => 'Some dummy value here', 35 | ]); 36 | 37 | $this->info('First record has been created.'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Helpers/TranslationHelper.php: -------------------------------------------------------------------------------- 1 | select('locale') 45 | ->pluck('locale'); 46 | 47 | return $locales; 48 | } 49 | 50 | /** 51 | * Remove folders recursively. 52 | * 53 | * @param string $dir 54 | * 55 | * @return void 56 | */ 57 | public static function removeDirectory($dir) 58 | { 59 | foreach (scandir($dir) as $file) { 60 | if ('.' === $file || '..' === $file) { 61 | continue; 62 | } 63 | 64 | if (is_dir($dir.'/'.$file)) { 65 | rmdir_recursive($dir.'/'.$file); 66 | } else { 67 | unlink($dir.'/'.$file); 68 | } 69 | } 70 | rmdir($dir); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Http/Controllers/GroupsController.php: -------------------------------------------------------------------------------- 1 | validate([ 41 | 'group' => 'required|string|unique:ltm_translations,group', 42 | ]); 43 | 44 | // Convert group name into slug format. 45 | $group = TranslationHelper::slug($data['group']); 46 | 47 | // Get supported locales. 48 | $locales = TranslationHelper::getLocales(); 49 | 50 | // Add a placeholder keyword for all locales. 51 | foreach ($locales as $locale) { 52 | Translation::firstOrCreate([ 53 | 'locale' => $locale, 54 | 'group' => $group, 55 | 'key' => 'placeholder', 56 | ]); 57 | } 58 | 59 | return response()->json([ 60 | 'success' => true, 61 | 'group' => $group, 62 | ]); 63 | } 64 | 65 | /** 66 | * Display the specified resource. 67 | * 68 | * @param string $group 69 | * @return \Illuminate\Http\Response 70 | */ 71 | public function show($group) 72 | { 73 | // 74 | } 75 | 76 | /** 77 | * Show the form for editing the specified resource. 78 | * 79 | * @param int $id 80 | * @return \Illuminate\Http\Response 81 | */ 82 | public function edit($id) 83 | { 84 | // 85 | } 86 | 87 | /** 88 | * Update the specified resource in storage. 89 | * 90 | * @param \Illuminate\Http\Request $request 91 | * @param Translation $group 92 | * @return \Illuminate\Http\Response 93 | */ 94 | public function update(Request $request, Translation $group) 95 | { 96 | // 97 | } 98 | 99 | /** 100 | * Remove the specified resource from storage. 101 | * 102 | * @param string $group 103 | * @return \Illuminate\Http\Response 104 | */ 105 | public function destroy($group) 106 | { 107 | // Get supported locales. 108 | $locales = TranslationHelper::getLocales(); 109 | 110 | // Delete all PHP translation files. 111 | foreach ($locales as $locale) { 112 | $file = resource_path('lang/'.$locale.'/').$group.'.php'; 113 | 114 | if (file_exists($file)) { 115 | unlink($file); 116 | } 117 | } 118 | 119 | // Delete translations from database. 120 | Translation::where('group', $group)->delete(); 121 | 122 | return response()->json([ 123 | 'success' => true, 124 | ]); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Http/Controllers/LocalesController.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 21 | } 22 | 23 | /** 24 | * Get all available locales. 25 | */ 26 | public function index() 27 | { 28 | // Merge app locale and translation locales. 29 | $locales = TranslationHelper::getLocales(); 30 | $defaultLocale = app()->getLocale(); 31 | 32 | return response()->json([ 33 | 'locales' => $locales, 34 | 'defaultLocale' => $defaultLocale, 35 | ]); 36 | } 37 | 38 | /** 39 | * Store a newly created resource in storage. 40 | * 41 | * @param \Illuminate\Http\Request $request 42 | * @return \Illuminate\Http\Response 43 | */ 44 | public function store() 45 | { 46 | $data = request()->validate([ 47 | 'locale' => 'required|string|unique:ltm_translations,locale', 48 | ]); 49 | 50 | // Get all keys. 51 | $keys = Translation::groupBy('group', 'key')->pluck('group', 'key'); 52 | 53 | // When there are keys found. 54 | if (count($keys) > 0) { 55 | 56 | // Create all translation records. 57 | foreach ($keys as $key => $group) { 58 | Translation::firstOrCreate([ 59 | 'status' => 0, 60 | 'locale' => $data['locale'], 61 | 'group' => $group, 62 | 'key' => $key, 63 | ]); 64 | } 65 | } else { 66 | Translation::firstOrCreate([ 67 | 'status' => 0, 68 | 'locale' => $data['locale'], 69 | 'group' => 'first', 70 | 'key' => 'placeholder', 71 | ]); 72 | } 73 | 74 | return response()->json([ 75 | 'success' => true, 76 | ]); 77 | } 78 | 79 | /** 80 | * Delete a locale. 81 | */ 82 | public function destroy(Request $request, $locale) 83 | { 84 | // Remove translations from database. 85 | Translation::where('locale', $locale)->delete(); 86 | 87 | // Unlink translation files. 88 | $dir = resource_path('lang/'.$locale.'/'); 89 | if (is_dir($dir)) { 90 | TranslationHelper::removeDirectory($dir); 91 | } 92 | 93 | return response()->json([ 94 | 'success' => true, 95 | ]); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Http/Controllers/TranslationsController.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 22 | } 23 | 24 | /** 25 | * Display a listing of the resource. 26 | * 27 | * @return \Illuminate\Http\Response 28 | */ 29 | public function index() 30 | { 31 | $groups = Translation::groupBy('group')->orderBy('group')->pluck('group'); 32 | 33 | return response()->json($groups); 34 | } 35 | 36 | /** 37 | * Show the form for creating a new resource. 38 | * 39 | * @return \Illuminate\Http\Response 40 | */ 41 | public function create() 42 | { 43 | // 44 | } 45 | 46 | /** 47 | * Store a newly created resource in storage. 48 | * 49 | * @param \Illuminate\Http\Request $request 50 | * @return \Illuminate\Http\Response 51 | */ 52 | public function store() 53 | { 54 | $data = request()->validate([ 55 | 'group' => 'required|string', 56 | 'keywords' => 'required', 57 | ]); 58 | 59 | // Create separate keywords from request data. 60 | $keys = explode("\n", request('keywords')); 61 | 62 | // Get supported locales. 63 | $locales = TranslationHelper::getLocales(); 64 | 65 | foreach ($keys as $key) { 66 | 67 | // Make sure no spaces are added. 68 | $key = trim($key); 69 | 70 | // Add the keyword for all locales. 71 | foreach ($locales as $locale) { 72 | Translation::firstOrCreate([ 73 | 'status' => 1, 74 | 'locale' => $locale, 75 | 'group' => $data['group'], 76 | 'key' => $key, 77 | ]); 78 | } 79 | } 80 | 81 | return response()->json([ 82 | 'success' => true, 83 | ]); 84 | } 85 | 86 | /** 87 | * Display the specified resource. 88 | * 89 | * @param string $group 90 | * @return \Illuminate\Http\Response 91 | */ 92 | public function show($group, $subgroup = null) 93 | { 94 | // Concat group and subgroup 95 | if (! is_null($subgroup)) { 96 | $group = $group.'/'.$subgroup; 97 | } 98 | 99 | // Get translations by group. 100 | $data = Translation::where('group', $group)->orderBy('key', 'asc')->get(); 101 | 102 | $translations = []; 103 | 104 | // Group translations by key. 105 | foreach ($data as $translation) { 106 | $translations[$translation->key][$translation->locale] = $translation; 107 | } 108 | 109 | return response()->json($translations); 110 | } 111 | 112 | /** 113 | * Show the form for editing the specified resource. 114 | * 115 | * @param int $id 116 | * @return \Illuminate\Http\Response 117 | */ 118 | public function edit($id) 119 | { 120 | // 121 | } 122 | 123 | /** 124 | * Update the specified resource in storage. 125 | * 126 | * @param \Illuminate\Http\Request $request 127 | * @param Translation $translation 128 | * @return \Illuminate\Http\Response 129 | */ 130 | public function update(Request $request, Translation $translation) 131 | { 132 | // Validate data. 133 | $data = $request->validate([ 134 | 'id' => 'required|numeric|min:1', 135 | 'value' => 'nullable|string', 136 | ]); 137 | 138 | // If an empty string is given, make sure it's null. 139 | $data['value'] = ($data['value'] == '') ? null : $data['value']; 140 | $data['status'] = 1; 141 | 142 | // Update the translation. 143 | $translation->update($data); 144 | 145 | return response()->json([ 146 | 'success' => true, 147 | ]); 148 | } 149 | 150 | /** 151 | * Update the specified keyword in storage. 152 | * 153 | * @param \Illuminate\Http\Request $request 154 | * @param 155 | * @return \Illuminate\Http\Response 156 | */ 157 | public function updateKey(Request $request) 158 | { 159 | // Validate data. 160 | $data = $request->validate([ 161 | 'group' => 'required|string', 162 | 'old_key' => 'nullable|string', 163 | 'new_key' => 'nullable|string', 164 | ]); 165 | 166 | // Update keys 167 | Translation::where('group', $data['group']) 168 | ->where('key', $data['old_key']) 169 | ->update([ 170 | 'key' => $data['new_key'], 171 | ]); 172 | 173 | return response()->json([ 174 | 'success' => true, 175 | ]); 176 | } 177 | 178 | /** 179 | * Remove the specified resource from storage. 180 | * 181 | * @param string $group 182 | * @param string $keyword 183 | * 184 | * @return \Illuminate\Http\Response 185 | */ 186 | public function destroy($group, $id) 187 | { 188 | // Get key by ID. 189 | $key = Translation::where('id', $id)->value('key'); 190 | 191 | if ($key) { 192 | // Delete translations 193 | Translation::where('group', $group)->where('key', $key)->delete(); 194 | } 195 | } 196 | 197 | /** 198 | * Export translations to PHP files. 199 | */ 200 | public function export(Request $request) 201 | { 202 | // Validate data. 203 | $data = $request->validate([ 204 | 'group' => 'required|string', 205 | ]); 206 | 207 | // When all groups should be exported. 208 | if ($data['group'] == '*') { 209 | $this->manager->exportAllTranslations(); 210 | 211 | // When a single group should be exported. 212 | } else { 213 | // Add support for JSON exports. 214 | $json = false; 215 | if ($data['group'] == '_json') { 216 | $json = true; 217 | } 218 | 219 | // Export translations to PHP files. 220 | $this->manager->exportTranslations($data['group'], $json); 221 | } 222 | 223 | // Create Vue i18n JSON file when needed. 224 | $this->exportVuei18n(); 225 | } 226 | 227 | /** 228 | * Export. 229 | */ 230 | public function exportVuei18n() 231 | { 232 | // When JSON files should be generated using the vue-i18n package. 233 | if (config('nova-translation-manager.vue-i18n.active')) { 234 | $job = 'vue-i18n:generate'; 235 | 236 | // When the file should be formatted with the --umd flag. 237 | if (config('nova-translation-manager.vue-i18n.umd')) { 238 | $job .= ' --umd'; 239 | } 240 | 241 | Artisan::call($job); 242 | } 243 | } 244 | 245 | /** 246 | * Import translations. 247 | */ 248 | public function import(Request $request) 249 | { 250 | // Validate data. 251 | $data = $request->validate([ 252 | 'type' => 'required|string|in:replace,append', 253 | ]); 254 | 255 | // Set replace flag. 256 | $replace = ($data['type'] == 'replace') ? true : false; 257 | 258 | $this->manager->importTranslations($replace); 259 | } 260 | 261 | /** 262 | * Fix missing translations. 263 | */ 264 | public function fix(Request $request) 265 | { 266 | // Validate data. 267 | $data = $request->validate([ 268 | 'key' => 'required|string', 269 | 'group' => 'required|string', 270 | ]); 271 | 272 | // Get supported locales. 273 | $locales = TranslationHelper::getLocales(); 274 | 275 | foreach ($locales as $locale) { 276 | Translation::firstOrCreate([ 277 | 'group' => $data['group'], 278 | 'key' => $data['key'], 279 | 'locale' => $locale, 280 | ]); 281 | } 282 | } 283 | 284 | /** 285 | * Fix missing translations for an entire group. 286 | */ 287 | public function fixGroup(Request $request) 288 | { 289 | // Validate data. 290 | $data = $request->validate([ 291 | 'group' => 'required|string', 292 | ]); 293 | 294 | // Get supported locales. 295 | $locales = TranslationHelper::getLocales(); 296 | 297 | // Get existing translations. 298 | $keys = Translation::select('group', 'key') 299 | ->where('group', $data['group']) 300 | ->groupBy('group') 301 | ->groupBy('key') 302 | ->pluck('key'); 303 | 304 | foreach ($locales as $locale) { 305 | foreach ($keys as $key) { 306 | Translation::firstOrCreate([ 307 | 'group' => $data['group'], 308 | 'key' => $key, 309 | 'locale' => $locale, 310 | ]); 311 | } 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/Http/Controllers/UploadController.php: -------------------------------------------------------------------------------- 1 | handle(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Http/Middleware/Authorize.php: -------------------------------------------------------------------------------- 1 | first([$this, 'matchesTool']); 20 | 21 | return optional($tool)->authorize($request) ? $next($request) : abort(403); 22 | } 23 | 24 | /** 25 | * Determine whether this tool belongs to the package. 26 | * 27 | * @param \Laravel\Nova\Tool $tool 28 | * @return bool 29 | */ 30 | public function matchesTool($tool) 31 | { 32 | return $tool instanceof LaravelNovaTranslationsManager; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Requests/UploadRequest.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 17 | } 18 | 19 | // Authorize request. 20 | public function authorize() : bool 21 | { 22 | return auth()->check(); 23 | } 24 | 25 | // Validation rules. 26 | public function rules() : array 27 | { 28 | return [ 29 | 'file' => 'required|file', 30 | 'publish' => 'required|string', 31 | ]; 32 | } 33 | 34 | // Handle request. 35 | public function handle() 36 | { 37 | $file = request()->file('file'); 38 | 39 | // Load the file into a string 40 | $string = @file_get_contents($file); 41 | 42 | // Convert all line-endings using regular expression 43 | $string = preg_replace('~\r\n?~', "\n", $string); 44 | file_put_contents($file, $string); 45 | 46 | $handle = fopen($file, 'r'); 47 | $header = true; 48 | 49 | while ($csvLine = fgetcsv($handle, 1000, ';')) { 50 | if ($header) { 51 | $header = false; 52 | } else { 53 | // Update translation. 54 | $translation = Translation::updateOrCreate([ 55 | 'locale' => $csvLine[0], 56 | 'group' => $csvLine[1], 57 | 'key' => $csvLine[2], 58 | ], [ 59 | 'value' => $csvLine[3], 60 | 'status' => 1, // Mark as updated, not published. 61 | ]); 62 | 63 | // Add group to publishable groups. 64 | if (! in_array($csvLine[1], $this->groups)) { 65 | $this->groups[] = $csvLine[1]; 66 | } 67 | } 68 | } 69 | 70 | if (request('publish') == 'false') { 71 | return; 72 | } 73 | 74 | // Publish all groups. 75 | foreach ($this->groups as $group) { 76 | $this->manager->exportTranslations($group, ($group == '_json')); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/LaravelNovaTranslationsManager.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/../resources/views', 'laravel-nova-translations-manager'); 22 | 23 | $this->publishConfiguration(); 24 | 25 | $this->app->booted(function () { 26 | $this->routes(); 27 | }); 28 | 29 | Nova::serving(function (ServingNova $event) { 30 | // 31 | }); 32 | } 33 | 34 | /** 35 | * Publish package configuration. 36 | * 37 | * @return void 38 | */ 39 | private function publishConfiguration() 40 | { 41 | $this->publishes([ 42 | __DIR__.'/../config/nova-translation-manager.php' => config_path('nova-translation-manager.php'), 43 | ], 'config'); 44 | } 45 | 46 | /** 47 | * Merge package configuration. 48 | * 49 | * @return void 50 | */ 51 | private function mergeConfiguration() 52 | { 53 | $this->mergeConfigFrom(__DIR__.'/../config/nova-translation-manager.php', 'nova-translation-manager'); 54 | } 55 | 56 | /** 57 | * Register the tool's routes. 58 | * 59 | * @return void 60 | */ 61 | protected function routes() 62 | { 63 | if ($this->app->routesAreCached()) { 64 | return; 65 | } 66 | 67 | Route::middleware(['nova', Authorize::class]) 68 | ->prefix('nova-vendor/laravel-nova-translations-manager') 69 | ->group(__DIR__.'/../routes/api.php'); 70 | } 71 | 72 | /** 73 | * Register any application services. 74 | * 75 | * @return void 76 | */ 77 | public function register() 78 | { 79 | $this->mergeConfiguration(); 80 | 81 | // Register commands. 82 | $this->app->singleton('command.translation-manager.install', function ($app) { 83 | return new InstallCommand($app['translation-manager']); 84 | }); 85 | 86 | $this->commands('command.translation-manager.install'); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix') 2 | 3 | mix 4 | .setPublicPath('dist') 5 | .js('resources/js/tool.js', 'js') 6 | .sass('resources/sass/tool.scss', 'css') 7 | --------------------------------------------------------------------------------