├── .eslintrc.js ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── translations.php ├── database ├── factories │ ├── ContributorFactory.php │ ├── InviteFactory.php │ ├── LanguageFactory.php │ ├── PhraseFactory.php │ ├── TranslationFactory.php │ └── TranslationFileFactory.php ├── migrations │ ├── add_is_root_to_translation_files_table.php │ ├── create_contributors_table.php │ ├── create_invites_table.php │ ├── create_languages_table.php │ ├── create_phrases_table.php │ ├── create_translation_files_table.php │ └── create_translations_table.php └── seeders │ └── LanguagesTableSeeder.php ├── package-lock.json ├── package.json ├── postcss.config.js ├── resources ├── css │ ├── app.scss │ ├── buttons.scss │ ├── vue-select.scss │ └── vue-tabs.scss ├── dist │ ├── .gitignore │ └── vendor │ │ ├── manifest.json │ │ └── translations-ui │ │ ├── assets │ │ ├── ExclamationCircleIcon-0d70cb72.js │ │ ├── XCircleIcon-d7d84b59.js │ │ ├── _plugin-vue_export-helper-c27b6911.js │ │ ├── accept-c0f3b371.js │ │ ├── add-source-key-a8cd429e.js │ │ ├── add-translation-26c8973a.js │ │ ├── alert.vue_vue_type_script_setup_true_lang-2df1d0f6.js │ │ ├── app-902477a6.css │ │ ├── app-e2c1adb4.js │ │ ├── base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js │ │ ├── dialog-e6df4d57.js │ │ ├── dialog.vue_vue_type_script_setup_true_lang-3928baa8.js │ │ ├── edit-1cc464ce.js │ │ ├── edit-a73610ef.js │ │ ├── edit-f95b33c7.js │ │ ├── error-aabeb525.js │ │ ├── flag.vue_vue_type_script_setup_true_lang-9a70fac7.js │ │ ├── forgot-password-f9c47d9b.js │ │ ├── icon-arrow-left-b70428ec.js │ │ ├── icon-arrow-right-e09bb615.js │ │ ├── icon-clipboard-327076f3.js │ │ ├── icon-close-5cb91d75.js │ │ ├── icon-key-8ffee4f6.js │ │ ├── icon-language-56d3f627.js │ │ ├── icon-pencil-8922e06b.js │ │ ├── icon-plus-4cbbfd59.js │ │ ├── icon-publish-8507a7dd.js │ │ ├── icon-trash-a2f124e2.js │ │ ├── index-109c8b07.js │ │ ├── index-503df9de.js │ │ ├── index-5103d7ef.js │ │ ├── index-a9f2d9ce.js │ │ ├── input-checkbox.vue_vue_type_script_setup_true_lang-cbb48805.js │ │ ├── input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js │ │ ├── input-native-select.vue_vue_type_script_setup_true_lang-ad2aa845.js │ │ ├── input-password.vue_vue_type_script_setup_true_lang-9dd0bcca.js │ │ ├── input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js │ │ ├── invite-f49efd85.js │ │ ├── invited-item-e0f423b9.js │ │ ├── invited-item.vue_vue_type_script_setup_true_lang-365068e6.js │ │ ├── invited-table-64fbaaaa.js │ │ ├── invited-table.vue_vue_type_script_setup_true_lang-ac2c147c.js │ │ ├── layout-dashboard.vue_vue_type_script_setup_true_lang-ce51e587.js │ │ ├── layout-guest-bd2cb4f5.js │ │ ├── lodash-293e1000.js │ │ ├── login-9a64952c.js │ │ ├── logo-e09fccf9.js │ │ ├── pagination.vue_vue_type_script_setup_true_lang-8ed7a73a.js │ │ ├── phrase-item-90a67d96.js │ │ ├── phrase-item.vue_vue_type_script_setup_true_lang-6c611958.js │ │ ├── publish-translations-5c498ad5.js │ │ ├── reset-password-12bd1497.js │ │ ├── source-phrase-item-34b7c08a.js │ │ ├── source-phrase-item.vue_vue_type_script_setup_true_lang-971d7c33.js │ │ ├── transition-314b73c3.js │ │ ├── translation-item-27ea6c78.js │ │ ├── translation-item.vue_vue_type_script_setup_true_lang-f94e4148.js │ │ ├── use-auth-46901eba.js │ │ ├── use-confirmation-dialog-9a866ad9.js │ │ ├── use-input-size-6b6f86f1.js │ │ ├── use-language-code-conversion-5d8d1906.js │ │ ├── user-update-password-form-0700c2a6.js │ │ ├── user-update-password-form.vue_vue_type_script_setup_true_lang-7b0268c2.js │ │ ├── user-update-profile-information-form-36922b8f.js │ │ └── user-update-profile-information-form.vue_vue_type_script_setup_true_lang-aa37d789.js │ │ └── manifest.json ├── favicon.ico ├── scripts │ ├── app.ts │ ├── composables │ │ ├── .gitkeep │ │ ├── use-auth.ts │ │ ├── use-button-size.ts │ │ ├── use-button-variant.ts │ │ ├── use-confirmation-dialog.ts │ │ ├── use-input-size.ts │ │ └── use-language-code-conversion.ts │ ├── plugins │ │ ├── .gitkeep │ │ └── notifications.ts │ ├── types │ │ ├── .gitkeep │ │ ├── auto-imports.d.ts │ │ ├── components.d.ts │ │ ├── global.d.ts │ │ └── index.d.ts │ └── utils │ │ ├── .gitkeep │ │ └── to-items.ts └── views │ ├── app.blade.php │ ├── components │ ├── alert.vue │ ├── base-button.vue │ ├── confirmation-dialog.vue │ ├── dialog.vue │ ├── empty-state.vue │ ├── flag.vue │ ├── form │ │ ├── input-checkbox.vue │ │ ├── input-combobox.vue │ │ ├── input-error.vue │ │ ├── input-file.vue │ │ ├── input-label.vue │ │ ├── input-multiselect.vue │ │ ├── input-native-select.vue │ │ ├── input-password.vue │ │ ├── input-select.vue │ │ ├── input-text.vue │ │ └── input-textarea.vue │ ├── icons │ │ ├── empty-states │ │ │ └── icon-empty-translations.vue │ │ ├── icon-arrow-left.vue │ │ ├── icon-arrow-right.vue │ │ ├── icon-check.vue │ │ ├── icon-clipboard.vue │ │ ├── icon-close.vue │ │ ├── icon-cog.vue │ │ ├── icon-document.vue │ │ ├── icon-ellipsis-vertical.vue │ │ ├── icon-external-link.vue │ │ ├── icon-eye-off.vue │ │ ├── icon-eye.vue │ │ ├── icon-google.vue │ │ ├── icon-key.vue │ │ ├── icon-language.vue │ │ ├── icon-loading.vue │ │ ├── icon-mail.vue │ │ ├── icon-pencil.vue │ │ ├── icon-plus.vue │ │ ├── icon-publish.vue │ │ ├── icon-similar.vue │ │ ├── icon-speak.vue │ │ ├── icon-star.vue │ │ ├── icon-sync.vue │ │ ├── icon-translation.vue │ │ ├── icon-trash.vue │ │ └── icon-versions.vue │ ├── illustrations │ │ └── app-launch.vue │ ├── logo.vue │ ├── modal.vue │ ├── pagination.vue │ ├── phrase │ │ ├── phrase-with-parameters.vue │ │ ├── phrases-filter.vue │ │ ├── similar │ │ │ ├── similar-phrases-item.vue │ │ │ └── similar-phrases.vue │ │ └── suggestions │ │ │ ├── machine-translate-item.vue │ │ │ └── machine-translate.vue │ └── slideover.vue │ ├── layouts │ ├── dashboard │ │ ├── layout-dashboard.vue │ │ └── partials │ │ │ └── navbar.vue │ └── guest │ │ └── layout-guest.vue │ ├── mail │ ├── invite.blade.php │ └── password.blade.php │ └── pages │ ├── auth │ ├── forgot-password.vue │ ├── invite │ │ └── accept.vue │ ├── login.vue │ └── reset-password.vue │ ├── contributor │ ├── index.vue │ ├── modals │ │ └── invite.vue │ └── partials │ │ ├── invited-item.vue │ │ └── invited-table.vue │ ├── error.vue │ ├── phrases │ ├── edit.vue │ ├── index.vue │ └── phrase-item.vue │ ├── profile │ ├── edit.vue │ └── partials │ │ ├── user-update-password-form.vue │ │ └── user-update-profile-information-form.vue │ ├── source │ ├── edit.vue │ ├── index.vue │ ├── modals │ │ └── add-source-key.vue │ └── source-phrase-item.vue │ └── translations │ ├── index.vue │ ├── modals │ ├── add-translation.vue │ └── publish-translations.vue │ └── translation-item.vue ├── routes └── web.php ├── src ├── Actions │ ├── CopyPhrasesFromSourceAction.php │ ├── CopySourceKeyToTranslationsAction.php │ ├── CreateSourceKeyAction.php │ ├── CreateTranslationForLanguageAction.php │ └── SyncPhrasesAction.php ├── Console │ └── Commands │ │ ├── CleanOldVersionCommand.php │ │ ├── ContributorCommand.php │ │ ├── ExportTranslationsCommand.php │ │ ├── ImportTranslationsCommand.php │ │ └── PublishCommand.php ├── Enums │ ├── RoleEnum.php │ └── StatusEnum.php ├── Exceptions │ └── TranslationsUIExceptionHandler.php ├── Http │ ├── Controllers │ │ ├── Auth │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── InvitationAcceptController.php │ │ │ ├── NewPasswordController.php │ │ │ └── PasswordResetLinkController.php │ │ ├── ContributorController.php │ │ ├── PhraseController.php │ │ ├── ProfileController.php │ │ ├── SourcePhraseController.php │ │ └── TranslationController.php │ ├── Middleware │ │ ├── Authenticate.php │ │ ├── HandleInertiaRequests.php │ │ └── RedirectIfNotOwner.php │ ├── Requests │ │ └── LoginRequest.php │ └── Resources │ │ ├── ContributorResource.php │ │ ├── InviteResource.php │ │ ├── LanguageResource.php │ │ ├── PhraseResource.php │ │ ├── TranslationFileResource.php │ │ ├── TranslationResource.php │ │ └── UnwrappedAnonymousResourceCollection.php ├── Mail │ ├── InviteCreated.php │ └── ResetPassword.php ├── Modal.php ├── Models │ ├── Contributor.php │ ├── Invite.php │ ├── Language.php │ ├── Phrase.php │ ├── Translation.php │ └── TranslationFile.php ├── Traits │ ├── HasDatabaseConnection.php │ └── HasUuid.php ├── TranslationsManager.php ├── TranslationsUIServiceProvider.php └── helpers.php ├── tailwind.config.js ├── tsconfig.json ├── vite.config.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/eslint-recommended", 5 | "plugin:vue/vue3-recommended", 6 | "plugin:tailwindcss/recommended", 7 | "prettier" 8 | ], 9 | plugins: ["unused-imports"], 10 | rules: { 11 | "vue/component-tags-order": [ 12 | "error", 13 | { 14 | order: ["script", "template", "style"] 15 | } 16 | ], 17 | "vue/multi-word-component-names": "off", 18 | "vue/component-api-style": ["error", ["script-setup", "composition"]], 19 | "vue/component-name-in-template-casing": "error", 20 | "vue/block-lang": [ 21 | "error", 22 | { 23 | script: { lang: "ts" } 24 | } 25 | ], 26 | "vue/define-macros-order": [ 27 | "warn", 28 | { 29 | order: ["defineProps", "defineEmits"] 30 | } 31 | ], 32 | "vue/define-emits-declaration": ["error", "type-based"], 33 | "vue/define-props-declaration": ["error", "type-based"], 34 | "vue/match-component-import-name": "error", 35 | "vue/no-ref-object-destructure": "error", 36 | "vue/no-unused-refs": "error", 37 | "vue/no-useless-v-bind": "error", 38 | "vue/padding-line-between-tags": "warn", 39 | "vue/prefer-separate-static-class": "error", 40 | "vue/prefer-true-attribute-shorthand": "error", 41 | "vue/no-v-html": "off", 42 | 43 | "tailwindcss/no-custom-classname": "off", 44 | 45 | "no-undef": "off", 46 | "no-unused-vars": "off", 47 | "no-console": ["warn"] 48 | }, 49 | ignorePatterns: ["*.d.ts"], 50 | parser: "vue-eslint-parser", 51 | parserOptions: { 52 | parser: "@typescript-eslint/parser" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | auto-imports.d.ts 2 | components.d.ts 3 | generated.d.ts 4 | routes.d.ts 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": false, 4 | "bracketSameLine": true, 5 | "printWidth": 500, 6 | "singleAttributePerLine": false, 7 | "singleQuote": false, 8 | "plugins": ["prettier-plugin-tailwindcss"] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) outhebox 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "outhebox/laravel-translations", 3 | "description": "Laravel Translations UI provides a simple way to manage your translations in your Laravel application. It allows you to add, edit, delete and export translations, and it also provides a search functionality to find translations.", 4 | "keywords": [ 5 | "laravel", 6 | "laravel-translations", 7 | "translations", 8 | "localization", 9 | "inertiajs", 10 | "inertia", 11 | "translation-manager" 12 | ], 13 | "homepage": "https://github.com/MohmmedAshraf/laravel-translations", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Mohamed Ashraf", 18 | "email": "cupo.ashraf@gmail.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.2|^8.3", 24 | "brick/varexporter": "^0.6.0", 25 | "inertiajs/inertia-laravel": "^2.0", 26 | "spatie/laravel-package-tools": "^1.0", 27 | "stichoza/google-translate-php": "^5.0", 28 | "tightenco/ziggy": "^2.5", 29 | "ext-zip": "*" 30 | }, 31 | "require-dev": { 32 | "laravel/pint": "^1.0", 33 | "nunomaduro/collision": "^8.0", 34 | "orchestra/testbench": "^9.0|^10.0", 35 | "pestphp/pest": "^3.0", 36 | "pestphp/pest-plugin-faker": "^3.0", 37 | "pestphp/pest-plugin-laravel": "^3.0" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Outhebox\\TranslationsUI\\": "src", 42 | "Outhebox\\TranslationsUI\\Database\\Factories\\": "database/factories", 43 | "Outhebox\\TranslationsUI\\Database\\Seeders\\": "database/seeders" 44 | }, 45 | "files": [ 46 | "src/helpers.php" 47 | ] 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Outhebox\\TranslationsUI\\Tests\\": "tests" 52 | } 53 | }, 54 | "scripts": { 55 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 56 | "analyse": "vendor/bin/phpstan analyse", 57 | "test": "vendor/bin/pest", 58 | "test-coverage": "vendor/bin/pest --coverage", 59 | "format": "vendor/bin/pint" 60 | }, 61 | "config": { 62 | "allow-plugins": { 63 | "pestphp/pest-plugin": true 64 | } 65 | }, 66 | "extra": { 67 | "laravel": { 68 | "providers": [ 69 | "Outhebox\\TranslationsUI\\TranslationsUIServiceProvider" 70 | ], 71 | "aliases": { 72 | "TranslationsUI": "Outhebox\\TranslationsUI\\Facades\\TranslationsUI" 73 | } 74 | } 75 | }, 76 | "minimum-stability": "stable", 77 | "prefer-stable": true 78 | } 79 | -------------------------------------------------------------------------------- /database/factories/ContributorFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name(), 16 | 'email' => $this->faker->unique()->safeEmail(), 17 | 'role' => $this->faker->randomElement(['admin', 'translator']), 18 | 'password' => bcrypt('password'), 19 | 'remember_token' => null, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/factories/InviteFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->uuid(), 17 | 'email' => $this->faker->unique()->safeEmail(), 18 | 'role' => $this->faker->randomElement(RoleEnum::cases()), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /database/factories/LanguageFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->boolean(), 16 | 'code' => $this->faker->randomElement(['en', 'nl', 'fr', 'de', 'es', 'it', 'pt', 'ru', 'ja', 'zh']), 17 | 'name' => $this->faker->randomElement(['English', 'Dutch', 'French', 'German', 'Spanish', 'Italian', 'Portuguese', 'Russian', 'Japanese', 'Chinese']), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/factories/PhraseFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->uuid(), 18 | 'key' => $this->faker->unique()->word(), 19 | 'translation_id' => Translation::factory(), 20 | 'translation_file_id' => TranslationFile::factory(), 21 | 'group' => $this->faker->word(), 22 | 'value' => $this->faker->sentence(), 23 | 'parameters' => [], 24 | ]; 25 | } 26 | 27 | public function withParameters(): self 28 | { 29 | return $this->state([ 30 | 'parameters' => [ 31 | 'param1', 32 | 'param2', 33 | ], 34 | ]); 35 | } 36 | 37 | public function withSource(): self 38 | { 39 | return $this->state([ 40 | 'phrase_id' => Phrase::factory(), 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/factories/TranslationFactory.php: -------------------------------------------------------------------------------- 1 | false, 17 | 'language_id' => Language::factory(), 18 | ]; 19 | } 20 | 21 | public function source(): self 22 | { 23 | return $this->state([ 24 | 'source' => true, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/factories/TranslationFileFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->randomElement(['app', 'auth', 'pagination', 'passwords', 'validation']), 16 | 'extension' => 'php', 17 | 'is_root' => false, 18 | ]; 19 | } 20 | 21 | public function json(): self 22 | { 23 | return $this->state([ 24 | 'extension' => 'json', 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/add_is_root_to_translation_files_table.php: -------------------------------------------------------------------------------- 1 | connection; 14 | } 15 | 16 | public function up(): void 17 | { 18 | Schema::table('ltu_translation_files', function (Blueprint $table) { 19 | $table->boolean('is_root')->default(false)->after('extension'); 20 | }); 21 | } 22 | 23 | public function down(): void 24 | { 25 | Schema::table('ltu_translation_files', function (Blueprint $table) { 26 | $table->dropColumn('is_root'); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/create_contributors_table.php: -------------------------------------------------------------------------------- 1 | connection; 14 | } 15 | 16 | public function up(): void 17 | { 18 | Schema::create('ltu_contributors', function (Blueprint $table) { 19 | $table->id(); 20 | $table->string('name'); 21 | $table->string('email')->unique(); 22 | $table->string('password'); 23 | $table->string('avatar')->nullable(); 24 | $table->tinyInteger('role')->nullable(); 25 | $table->rememberToken(); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('ltu_contributors'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/create_invites_table.php: -------------------------------------------------------------------------------- 1 | connection; 15 | } 16 | 17 | public function up(): void 18 | { 19 | Schema::create('ltu_invites', function (Blueprint $table) { 20 | $table->id(); 21 | $table->string('email')->unique(); 22 | $table->string('token', 32)->unique(); 23 | $table->tinyInteger('role')->default(RoleEnum::translator->value); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('ltu_invites'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/create_languages_table.php: -------------------------------------------------------------------------------- 1 | connection; 14 | } 15 | 16 | public function up(): void 17 | { 18 | Schema::create('ltu_languages', function (Blueprint $table) { 19 | $table->id(); 20 | $table->string('name'); 21 | $table->string('code')->index(); 22 | $table->boolean('rtl')->default(false); 23 | }); 24 | } 25 | 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('ltu_languages'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/create_phrases_table.php: -------------------------------------------------------------------------------- 1 | connection; 18 | } 19 | 20 | public function up(): void 21 | { 22 | Schema::create('ltu_phrases', function (Blueprint $table) { 23 | $table->id(); 24 | $table->char('uuid', 36); 25 | $table->foreignIdFor(Translation::class)->constrained('ltu_translations')->cascadeOnDelete(); 26 | $table->foreignIdFor(TranslationFile::class)->constrained('ltu_translation_files')->cascadeOnDelete(); 27 | $table->foreignIdFor(Phrase::class)->nullable()->constrained('ltu_phrases')->cascadeOnDelete(); 28 | $table->string('key'); 29 | $table->string('group'); 30 | $table->text('value')->nullable(); 31 | $table->string('status')->default(StatusEnum::active->value); 32 | $table->json('parameters')->nullable(); 33 | $table->text('note')->nullable(); 34 | $table->timestamps(); 35 | }); 36 | } 37 | 38 | public function down(): void 39 | { 40 | Schema::dropIfExists('ltu_phrases'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /database/migrations/create_translation_files_table.php: -------------------------------------------------------------------------------- 1 | connection; 14 | } 15 | 16 | public function up(): void 17 | { 18 | Schema::create('ltu_translation_files', function (Blueprint $table) { 19 | $table->id(); 20 | $table->string('name'); 21 | $table->string('extension'); 22 | }); 23 | } 24 | 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('ltu_translation_files'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/create_translations_table.php: -------------------------------------------------------------------------------- 1 | connection; 14 | } 15 | 16 | public function up(): void 17 | { 18 | Schema::create('ltu_translations', function (Blueprint $table) { 19 | $table->id(); 20 | $table->foreignId('language_id'); 21 | $table->boolean('source')->default(false); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('ltu_translations'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build" 6 | }, 7 | "devDependencies": { 8 | "@headlessui/vue": "^1.7.16", 9 | "@heroicons/vue": "^2.0.18", 10 | "@inertiajs/vue3": "^2.0.5", 11 | "@tailwindcss/forms": "^0.5.7", 12 | "@types/lodash": "^4.14.202", 13 | "@typescript-eslint/eslint-plugin": "^6.19.0", 14 | "@typescript-eslint/parser": "^6.19.0", 15 | "@vitejs/plugin-vue": "^4.5.2", 16 | "@vue/server-renderer": "^3.2.31", 17 | "@vueuse/components": "^10.7.2", 18 | "@vueuse/core": "^10.4.1", 19 | "autoprefixer": "^10.4.16", 20 | "axios": "^1.6.2", 21 | "country-code-emoji": "^2.3.0", 22 | "eslint": "^8.50.0", 23 | "eslint-config-prettier": "^9.1.0", 24 | "eslint-plugin-tailwindcss": "^3.14.0", 25 | "eslint-plugin-unused-imports": "^3.0.0", 26 | "eslint-plugin-vue": "^9.20.1", 27 | "floating-vue": "^2.0.0-beta.24", 28 | "laravel-vite-plugin": "^0.8.1", 29 | "lodash": "^4.17.21", 30 | "mitt": "^3.0.1", 31 | "momentum-modal": "^0.2.1", 32 | "os": "^0.1.2", 33 | "path": "^0.12.7", 34 | "postcss": "^8.4.32", 35 | "postcss-nesting": "^12.0.1", 36 | "prettier": "^3.0.3", 37 | "prettier-plugin-tailwindcss": "^0.5.4", 38 | "sass": "^1.68.0", 39 | "tailwindcss": "^3.3.6", 40 | "twemoji": "^14.0.2", 41 | "typescript": "^5.0.2", 42 | "unplugin-auto-import": "^0.16.6", 43 | "unplugin-vue-components": "^0.25.2", 44 | "unplugin-vue-define-options": "^1.4.1", 45 | "vite": "^4.0.0", 46 | "vite-plugin-eslint": "^1.8.1", 47 | "vite-plugin-watch": "^0.2.0", 48 | "vue": "^3.3.11", 49 | "vue-select": "^4.0.0-beta.6", 50 | "vue-toastification": "^2.0.0-rc.5", 51 | "vue-tsc": "^1.2.0", 52 | "vue3-tabs-component": "^1.3.7", 53 | "ziggy-js": "^2.1.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /resources/css/app.scss: -------------------------------------------------------------------------------- 1 | @import "buttons"; 2 | @import "vue-select"; 3 | @import "vue-tabs"; 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | @layer utilities { 9 | #app { 10 | @apply h-full; 11 | } 12 | } 13 | 14 | @layer components { 15 | .headless-toggle { 16 | @apply relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out; 17 | 18 | &:focus { 19 | @apply outline-none ring-2 ring-indigo-600 ring-offset-2; 20 | } 21 | 22 | .handle { 23 | @apply pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out; 24 | } 25 | 26 | .icon-holder { 27 | @apply absolute inset-0 flex h-full w-full items-center justify-center transition-opacity; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /resources/css/buttons.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | @apply relative inline-flex items-center justify-center overflow-hidden rounded-lg border font-semibold; 3 | 4 | &:disabled { 5 | @apply opacity-50 cursor-not-allowed; 6 | } 7 | 8 | &:focus-visible { 9 | @apply outline outline-2 outline-offset-2; 10 | } 11 | 12 | &.btn-xs { 13 | @apply gap-x-1.5 px-4 py-1 text-sm; 14 | } 15 | 16 | &.btn-sm { 17 | @apply gap-x-1.5 px-4 py-1.5 text-sm; 18 | } 19 | 20 | &.btn-md { 21 | @apply gap-x-1.5 px-6 py-2 text-sm; 22 | } 23 | 24 | &.btn-lg { 25 | @apply gap-x-2 px-8 py-2.5 text-sm; 26 | } 27 | 28 | &.btn-xl { 29 | @apply gap-x-2.5 px-8 py-3 text-base; 30 | } 31 | 32 | &.btn-primary { 33 | @apply border-blue-600 bg-blue-600 text-white; 34 | 35 | &:hover { 36 | @apply bg-blue-700; 37 | } 38 | 39 | &:focus-visible { 40 | @apply outline-blue-700; 41 | } 42 | } 43 | 44 | &.btn-dark { 45 | @apply border-gray-900 bg-gray-900 text-white; 46 | 47 | &:hover { 48 | @apply bg-gray-700; 49 | } 50 | 51 | &:focus-visible { 52 | @apply outline-gray-700; 53 | } 54 | } 55 | 56 | &.btn-secondary { 57 | @apply border-gray-300 bg-white text-gray-900; 58 | 59 | &:hover { 60 | @apply bg-gray-100; 61 | } 62 | 63 | &:focus-visible { 64 | @apply outline-gray-100; 65 | } 66 | } 67 | 68 | &.btn-secondary-darker { 69 | @apply border-gray-300 bg-gray-300 text-gray-700; 70 | 71 | &:hover { 72 | @apply bg-gray-200; 73 | } 74 | 75 | &:focus-visible { 76 | @apply outline-gray-200; 77 | } 78 | } 79 | 80 | &.btn-success { 81 | @apply border-green-600 bg-green-600 text-white; 82 | 83 | &:hover { 84 | @apply bg-green-500; 85 | } 86 | 87 | &:focus-visible { 88 | @apply outline-green-500; 89 | } 90 | } 91 | 92 | &.btn-danger { 93 | @apply border-red-600 bg-red-600 text-white; 94 | 95 | &:hover { 96 | @apply bg-red-500; 97 | } 98 | 99 | &:focus-visible { 100 | @apply outline-red-500; 101 | } 102 | } 103 | 104 | &.btn-warning { 105 | @apply border-yellow-600 bg-yellow-600 text-white; 106 | 107 | &:hover { 108 | @apply bg-yellow-500; 109 | } 110 | 111 | &:focus-visible { 112 | @apply outline-yellow-500; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /resources/css/vue-select.scss: -------------------------------------------------------------------------------- 1 | .vs__selected { 2 | @apply flex items-center gap-1 bg-blue-50 text-blue-600 text-sm font-medium border-blue-400 m-0 px-1.5 py-0.5 rounded-md #{!important}; 3 | } 4 | 5 | .vs__deselect { 6 | @apply fill-blue-400 hover:fill-red-600 #{!important}; 7 | } 8 | 9 | .vs__dropdown-toggle { 10 | @apply px-2 py-1.5 rounded-md border-gray-300 shadow-sm placeholder:text-sm placeholder:text-gray-400 #{!important}; 11 | } 12 | 13 | .vs__selected-options { 14 | @apply flex flex-wrap gap-1.5 #{!important}; 15 | } 16 | 17 | .vs__dropdown-menu { 18 | @apply mt-0.5 rounded-md shadow-lg #{!important}; 19 | } 20 | 21 | .vs--open { 22 | .vs__dropdown-toggle { 23 | @apply bg-blue-50 text-blue-600 border-blue-400 #{!important}; 24 | } 25 | 26 | .vs__actions > svg { 27 | @apply fill-blue-600 #{!important}; 28 | } 29 | } 30 | 31 | .vs__actions { 32 | @apply px-1 py-2 #{!important}; 33 | } 34 | 35 | .vs__dropdown-option { 36 | @apply flex items-center px-2.5 py-1.5 gap-2 #{!important}; 37 | } 38 | 39 | .vs__dropdown-option--disabled { 40 | @apply hidden #{!important}; 41 | } 42 | 43 | .v-popper--theme-tooltip { 44 | @apply text-sm #{!important}; 45 | } 46 | 47 | .v-popper__inner { 48 | @apply py-1 px-3 rounded-md border-none max-w-[300px] #{!important}; 49 | } 50 | 51 | .vs__search { 52 | @apply my-0.5 text-gray-400 placeholder:text-sm focus:placeholder:text-blue-500 #{!important}; 53 | } 54 | 55 | .vs__no-options { 56 | @apply text-sm py-2 px-3 text-gray-400 #{!important}; 57 | } 58 | -------------------------------------------------------------------------------- /resources/css/vue-tabs.scss: -------------------------------------------------------------------------------- 1 | .tabs-component { 2 | @apply mt-12 overflow-hidden rounded-md bg-white shadow; 3 | 4 | .tabs-component-tabs { 5 | @apply flex border-b bg-gray-100; 6 | 7 | .tabs-component-tab { 8 | @apply flex items-center gap-2 text-sm font-medium text-gray-500 cursor-pointer hover:bg-gray-100 uppercase transition-colors duration-200 ease-in-out; 9 | 10 | &:hover { 11 | @apply text-blue-600; 12 | } 13 | 14 | &.is-active { 15 | @apply text-blue-600 bg-white; 16 | } 17 | 18 | .tabs-component-tab-a { 19 | @apply flex items-center px-4 py-3 gap-2; 20 | } 21 | } 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/dist/.gitignore: -------------------------------------------------------------------------------- 1 | !vendor 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/ExclamationCircleIcon-0d70cb72.js: -------------------------------------------------------------------------------- 1 | import{o as e,h as r,f as a}from"./app-e2c1adb4.js";function n(o,t){return e(),r("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[a("path",{"fill-rule":"evenodd",d:"M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z","clip-rule":"evenodd"})])}export{n as r}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/XCircleIcon-d7d84b59.js: -------------------------------------------------------------------------------- 1 | import{o as e,h as a,f as r}from"./app-e2c1adb4.js";function n(o,l){return e(),a("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[r("path",{"fill-rule":"evenodd",d:"M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z","clip-rule":"evenodd"})])}export{n as r}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/_plugin-vue_export-helper-c27b6911.js: -------------------------------------------------------------------------------- 1 | const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/alert.vue_vue_type_script_setup_true_lang-2df1d0f6.js: -------------------------------------------------------------------------------- 1 | import{o as t,h as n,f as e,d as m,v as o,c as f,G as l,u as a,a3 as p,k as v}from"./app-e2c1adb4.js";import{r as x}from"./ExclamationCircleIcon-0d70cb72.js";import{r as g}from"./XCircleIcon-d7d84b59.js";function h(s,r){return t(),n("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[e("path",{"fill-rule":"evenodd",d:"M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z","clip-rule":"evenodd"})])}function w(s,r){return t(),n("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true","data-slot":"icon"},[e("path",{"fill-rule":"evenodd",d:"M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z","clip-rule":"evenodd"})])}const _={class:"flex items-start gap-2"},b={class:"mt-0.5 shrink-0"},y={class:"flex-1 md:flex md:justify-between"},C={class:"text-sm"},M=m({__name:"alert",props:{variant:{default:"info"}},setup(s){const r=s,d=o(()=>({info:"bg-blue-50 border border-blue-500 text-blue-700",success:"bg-green-50 border border-green-500 text-green-700",warning:"bg-yellow-50 border border-yellow-500 text-yellow-700",error:"bg-red-50 border border-red-500 text-red-700"})[r.variant]),i=o(()=>({info:"text-blue-400",success:"text-green-400",warning:"text-yellow-400",error:"text-red-400"})[r.variant]),c=o(()=>({info:w,success:h,warning:x,error:g})[r.variant]);return(u,B)=>(t(),n("div",{class:l(["rounded-md px-2 py-2.5",[a(d)]])},[e("div",_,[e("div",b,[(t(),f(p(a(c)),{class:l(["size-4",a(i)]),"aria-hidden":"true"},null,8,["class"]))]),e("div",y,[e("p",C,[v(u.$slots,"default")])])])],2))}});export{M as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js: -------------------------------------------------------------------------------- 1 | import{_ as p}from"./_plugin-vue_export-helper-c27b6911.js";import{o as n,h as a,f as o,v as l,d as _,k as f,G as r,b as m,e as b,u as i}from"./app-e2c1adb4.js";const g={},y={xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},h=o("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor","stroke-width":"4"},null,-1),C=o("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"},null,-1),z=[h,C];function w(s,e){return n(),a("svg",y,z)}const v=p(g,[["render",w]]),k=s=>({sizeClass:l(()=>({xs:"btn-xs",sm:"btn-sm",md:"btn-md",lg:"btn-lg"})[s])}),B=s=>({variantClass:l(()=>({primary:"btn-primary",secondary:"btn-secondary",success:"btn-success",danger:"btn-danger",warning:"btn-warning",dark:"btn-dark"})[s])}),x=["type","disabled"],V={key:0,class:"absolute left-0 top-0 flex size-full items-center justify-center"},S=_({__name:"base-button",props:{size:{default:"lg"},type:{default:"button"},variant:{default:"primary"},fullWidth:{type:Boolean,default:!1},isLoading:{type:Boolean,default:!1}},setup(s){const e=s,{sizeClass:c}=k(e.size),{variantClass:d}=B(e.variant);return(t,L)=>{const u=v;return n(),a("button",{type:t.type,class:r(["btn",[i(c),i(d),{"w-full":t.fullWidth}]]),disabled:t.isLoading},[o("span",{class:r(["flex items-center gap-2",{"opacity-0":t.isLoading}])},[f(t.$slots,"default")],2),t.isLoading?(n(),a("span",V,[m(u,{class:"size-6 animate-spin text-white"})])):b("",!0)],10,x)}}});export{S as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/dialog.vue_vue_type_script_setup_true_lang-3928baa8.js: -------------------------------------------------------------------------------- 1 | import{d as p,v as u,o as x,c as _,w as a,b as t,u as e,f as s,G as v,k as w,H as h}from"./app-e2c1adb4.js";import{k as l,S as y}from"./transition-314b73c3.js";import{G as g,U as b}from"./dialog-e6df4d57.js";const k=s("div",{class:"fixed inset-0 bg-gray-500/25 backdrop-blur transition-opacity"},null,-1),C={class:"fixed inset-0 overflow-y-auto"},z={class:"flex min-h-full items-center justify-center p-4 text-center"},L=p({__name:"dialog",props:{size:{default:"lg"},closeable:{type:Boolean,default:!0}},setup(r){const o=r,{show:i,close:n,redirect:c}=h(),d=()=>{o.closeable&&n()},m=u(()=>({sm:"sm:max-w-sm",md:"sm:max-w-md",lg:"sm:max-w-lg",xl:"sm:max-w-xl","2xl":"sm:max-w-2xl"})[o.size]);return(f,B)=>(x(),_(e(y),{appear:"",as:"template",show:e(i)},{default:a(()=>[t(e(b),{as:"div",class:"relative z-50",onClose:d},{default:a(()=>[t(e(l),{as:"template","leave-to":"opacity-0",enter:"duration-300 ease-out","enter-from":"opacity-0","enter-to":"opacity-100",leave:"duration-200 ease-in","leave-from":"opacity-100",onAfterLeave:e(c)},{default:a(()=>[k]),_:1},8,["onAfterLeave"]),s("div",C,[s("div",z,[t(e(l),{as:"template",enter:"duration-300 ease-out","enter-from":"opacity-0 scale-95","enter-to":"opacity-100 scale-100",leave:"duration-200 ease-in","leave-from":"opacity-100 scale-100","leave-to":"opacity-0 scale-95"},{default:a(()=>[t(e(g),{class:v([[e(m)],"w-full rounded-2xl bg-white text-left align-middle shadow-xl transition-all"])},{default:a(()=>[w(f.$slots,"default")]),_:3},8,["class"])]),_:3})])])]),_:3})]),_:3},8,["show"]))}});export{L as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/edit-f95b33c7.js: -------------------------------------------------------------------------------- 1 | import{_ as i}from"./layout-dashboard.vue_vue_type_script_setup_true_lang-ce51e587.js";import{d as r,o as p,h as n,b as t,w as _,f as s,F as l,m as c}from"./app-e2c1adb4.js";import{_ as d}from"./user-update-password-form.vue_vue_type_script_setup_true_lang-7b0268c2.js";import{_ as u}from"./user-update-profile-information-form.vue_vue_type_script_setup_true_lang-aa37d789.js";import"./icon-publish-8507a7dd.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./logo-e09fccf9.js";import"./use-auth-46901eba.js";import"./transition-314b73c3.js";import"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import"./input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js";import"./icon-close-5cb91d75.js";import"./input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js";import"./use-input-size-6b6f86f1.js";const f={class:"mx-auto max-w-2xl space-y-8 py-10 sm:px-6 lg:px-8"},h={class:"bg-white p-4 shadow sm:rounded-lg sm:p-8"},g={class:"bg-white p-4 shadow sm:rounded-lg sm:p-8"},e="Profile Settings",P=r({__name:"edit",props:{mustVerifyEmail:{type:Boolean},status:{}},setup(y){return(o,w)=>{const a=c,m=i;return p(),n(l,null,[t(a,{title:e}),t(m,{"page-title":e},{default:_(()=>[s("div",f,[s("div",h,[t(u,{"must-verify-email":o.mustVerifyEmail,status:o.status},null,8,["must-verify-email","status"])]),s("div",g,[t(d)])])]),_:1})],64)}}});export{P as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/error-aabeb525.js: -------------------------------------------------------------------------------- 1 | import{d as l,o as r,h as c,b as s,f as t,t as o,w as d,F as m,m as _,P as p}from"./app-e2c1adb4.js";import{_ as f}from"./icon-arrow-left-b70428ec.js";import"./_plugin-vue_export-helper-c27b6911.js";const u={class:"grid min-h-full place-items-center bg-white px-6 py-24 sm:py-32 lg:px-8"},h={class:"text-center"},g={class:"text-base font-semibold text-indigo-600"},x={class:"mt-4 text-3xl font-bold tracking-tight text-gray-900 sm:text-5xl"},b={class:"mt-6 text-base leading-7 text-gray-600"},y={class:"mt-10 flex items-center justify-center gap-x-6"},k=t("span",null,"Go back home",-1),F=l({__name:"error",props:{code:{default:"404"},title:{default:"Page not found"},text:{default:"Sorry, we couldn't find the page you're looking for."}},setup(w){return(e,B)=>{const n=_,a=f,i=p;return r(),c(m,null,[s(n,{title:e.title},null,8,["title"]),t("main",u,[t("div",h,[t("span",g,o(e.code),1),t("h1",x,o(e.title),1),t("span",b,o(e.text),1),t("div",y,[s(i,{href:e.route("ltu.translation.index"),class:"btn btn-md btn-primary"},{default:d(()=>[s(a,{class:"size-5"}),k]),_:1},8,["href"])])])])],64)}}});export{F as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/forgot-password-f9c47d9b.js: -------------------------------------------------------------------------------- 1 | import{_ as h}from"./layout-guest-bd2cb4f5.js";import{d as v,C as x,o as m,c as l,w as s,a as o,b as t,t as V,e as k,f as n,u as a,g as B,m as $,P as C}from"./app-e2c1adb4.js";import{_ as E}from"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import{_ as N,a as I}from"./input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js";import{_ as L}from"./input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js";import{_ as S}from"./alert.vue_vue_type_script_setup_true_lang-2df1d0f6.js";import"./logo-e09fccf9.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./icon-close-5cb91d75.js";import"./use-input-size-6b6f86f1.js";import"./ExclamationCircleIcon-0d70cb72.js";import"./XCircleIcon-d7d84b59.js";const G={class:"space-y-1"},P={class:"mt-8 flex w-full justify-center"},O=v({__name:"forgot-password",props:{status:{}},setup(R){const e=x({email:""}),u=()=>{e.post(route("ltu.password.email"),{onFinish:()=>{e.reset()}})};return(r,i)=>{const c=$,p=S,_=N,d=L,f=I,y=E,g=C,w=h;return m(),l(w,null,{title:s(()=>[o(" Reset your password ")]),subtitle:s(()=>[o(" Enter your email address below, and we'll send you instructions on how to reset your password. ")]),default:s(()=>[t(c,{title:"Reset your password"}),r.status?(m(),l(p,{key:0,variant:"success",class:"mb-4 w-full"},{default:s(()=>[o(V(r.status),1)]),_:1})):k("",!0),n("form",{class:"space-y-6",onSubmit:B(u,["prevent"])},[n("div",G,[t(_,{for:"email",value:"Email",class:"sr-only"}),t(d,{id:"email",modelValue:a(e).email,"onUpdate:modelValue":i[0]||(i[0]=b=>a(e).email=b),error:a(e).errors.email,type:"email",required:"",autofocus:"",autocomplete:"username",placeholder:"Email address",class:"bg-gray-50"},null,8,["modelValue","error"]),t(f,{message:a(e).errors.email},null,8,["message"])]),t(y,{type:"submit",variant:"secondary","is-loading":a(e).processing,"full-width":""},{default:s(()=>[o(" Send reset instructions ")]),_:1},8,["is-loading"])],32),n("div",P,[t(g,{href:r.route("ltu.login"),class:"text-xs font-medium text-gray-500 hover:text-blue-500"},{default:s(()=>[o(" Go back to sign in ")]),_:1},8,["href"])])]),_:1})}}});export{O as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-arrow-left-b70428ec.js: -------------------------------------------------------------------------------- 1 | import{_ as e}from"./_plugin-vue_export-helper-c27b6911.js";import{o,h as t,f as n}from"./app-e2c1adb4.js";const r={},s={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true"},_=n("path",{"fill-rule":"evenodd",d:"M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z","clip-rule":"evenodd"},null,-1),c=[_];function l(a,i){return o(),t("svg",s,c)}const f=e(r,[["render",l]]);export{f as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-arrow-right-e09bb615.js: -------------------------------------------------------------------------------- 1 | import{_ as e}from"./_plugin-vue_export-helper-c27b6911.js";import{o,h as t,f as n}from"./app-e2c1adb4.js";const r={},s={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true"},_=n("path",{"fill-rule":"evenodd",d:"M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z","clip-rule":"evenodd"},null,-1),c=[_];function a(l,i){return o(),t("svg",s,c)}const f=e(r,[["render",a]]);export{f as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-clipboard-327076f3.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./_plugin-vue_export-helper-c27b6911.js";import{o as e,h as t,f as s}from"./app-e2c1adb4.js";const n={},_={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 -960 960 960",fill:"currentColor"},c=s("path",{d:"M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z"},null,-1),r=[c];function a(h,v){return e(),t("svg",_,r)}const m=o(n,[["render",a]]);export{m as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-close-5cb91d75.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./_plugin-vue_export-helper-c27b6911.js";import{o as e,h as t,f as n}from"./app-e2c1adb4.js";const s={},r={xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor"},_=n("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M6 18 18 6M6 6l12 12"},null,-1),c=[_];function i(l,a){return e(),t("svg",r,c)}const f=o(s,[["render",i]]);export{f as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-key-8ffee4f6.js: -------------------------------------------------------------------------------- 1 | import{d,S as c,i as u,p as i,U as _,o as t,h as a,G as m,f as l}from"./app-e2c1adb4.js";import{_ as h}from"./_plugin-vue_export-helper-c27b6911.js";const p=["rows"],V=d({__name:"input-textarea",props:c({error:{},rows:{}},{modelValue:{}}),emits:["update:modelValue"],setup(o){const e=u(o,"modelValue");return(r,s)=>i((t(),a("textarea",{"onUpdate:modelValue":s[0]||(s[0]=n=>e.value=n),rows:r.rows??5,class:m(["w-full rounded-md border-gray-300 shadow-sm placeholder:text-sm placeholder:text-gray-400 focus:border-blue-500 focus:ring-blue-500",{"border-red-300 text-red-900 placeholder:text-red-300 focus:border-red-500 focus:ring-red-500":r.error}])},null,10,p)),[[_,e.value]])}}),f={},v={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true"},w=l("path",{d:"M0 0h24v24H0V0z",fill:"none"},null,-1),x=l("path",{d:"M22 19h-6v-4h-2.68c-1.14 2.42-3.6 4-6.32 4-3.86 0-7-3.14-7-7s3.14-7 7-7c2.72 0 5.17 1.58 6.32 4H24v6h-2v4zm-4-2h2v-4h2v-2H11.94l-.23-.67C11.01 8.34 9.11 7 7 7c-2.76 0-5 2.24-5 5s2.24 5 5 5c2.11 0 4.01-1.34 4.71-3.33l.23-.67H18v4zM7 15c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3zm0-4c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z"},null,-1),g=[w,x];function b(o,e){return t(),a("svg",v,g)}const B=h(f,[["render",b]]);export{B as _,V as a}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-language-56d3f627.js: -------------------------------------------------------------------------------- 1 | import{_ as t}from"./_plugin-vue_export-helper-c27b6911.js";import{o as e,h as n,f as o}from"./app-e2c1adb4.js";const s={},_={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},c=o("path",{d:"M0 0h24v24H0z",fill:"none"},null,-1),l=o("path",{d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"},null,-1),h=[c,l];function r(a,i){return e(),n("svg",_,h)}const f=t(s,[["render",r]]);export{f as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-pencil-8922e06b.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./_plugin-vue_export-helper-c27b6911.js";import{o as e,h as t,f as n}from"./app-e2c1adb4.js";const s={},r={xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor"},_=n("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"},null,-1),c=[_];function a(i,l){return e(),t("svg",r,c)}const m=o(s,[["render",a]]);export{m as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-plus-4cbbfd59.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./_plugin-vue_export-helper-c27b6911.js";import{o as e,h as t,f as n}from"./app-e2c1adb4.js";const s={},r={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor","aria-hidden":"true"},_=n("path",{d:"M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"},null,-1),c=[_];function a(h,i){return e(),t("svg",r,c)}const p=o(s,[["render",a]]);export{p as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-publish-8507a7dd.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./_plugin-vue_export-helper-c27b6911.js";import{o as t,h as e,f as s}from"./app-e2c1adb4.js";const n={},_={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 -960 960 960",fill:"currentColor"},c=s("path",{d:"M440-82q-76-8-141.5-41.5t-114-87Q136-264 108-333T80-480q0-91 36.5-168T216-780h-96v-80h240v240h-80v-109q-55 44-87.5 108.5T160-480q0 123 80.5 212.5T440-163v81Zm-17-214L254-466l56-56 113 113 227-227 56 57-283 283Zm177 196v-240h80v109q55-45 87.5-109T800-480q0-123-80.5-212.5T520-797v-81q152 15 256 128t104 270q0 91-36.5 168T744-180h96v80H600Z"},null,-1),r=[c];function h(a,l){return t(),e("svg",_,r)}const m=o(n,[["render",h]]);export{m as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/icon-trash-a2f124e2.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./_plugin-vue_export-helper-c27b6911.js";import{o as e,h as t,f as n}from"./app-e2c1adb4.js";const s={},r={xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24","stroke-width":"1.5",stroke:"currentColor"},c=n("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"},null,-1),_=[c];function a(m,i){return e(),t("svg",r,_)}const p=o(s,[["render",a]]);export{p as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/input-checkbox.vue_vue_type_script_setup_true_lang-cbb48805.js: -------------------------------------------------------------------------------- 1 | import{d as t,S as u,i as n,p as d,a8 as r,o as c,h as p}from"./app-e2c1adb4.js";const m=["value"],v=t({__name:"input-checkbox",props:u({value:{}},{modelValue:{type:Boolean}}),emits:["update:modelValue"],setup(a){const e=n(a,"modelValue");return(l,o)=>d((c(),p("input",{"onUpdate:modelValue":o[0]||(o[0]=s=>e.value=s),value:l.value,type:"checkbox",class:"rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500"},null,8,m)),[[r,e.value]])}});export{v as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js: -------------------------------------------------------------------------------- 1 | import{_ as i}from"./icon-close-5cb91d75.js";import{d as a,p as l,V as p,o as s,h as t,f as o,b as d,t as n,k as m}from"./app-e2c1adb4.js";const u={class:"flex items-center gap-1"},f={class:"flex size-4 items-center justify-center rounded-full border border-red-600"},h={class:"text-sm text-red-600"},y=a({__name:"input-error",props:{message:{}},setup(r){return(e,_)=>{const c=i;return l((s(),t("div",u,[o("div",f,[d(c,{class:"size-3 text-red-600"})]),o("p",h,n(e.message),1)],512)),[[p,e.message]])}}}),g={class:"block text-sm font-medium text-gray-700"},v={key:0},b={key:1},$=a({__name:"input-label",props:{value:{}},setup(r){return(e,_)=>(s(),t("label",g,[e.value?(s(),t("span",v,n(e.value),1)):(s(),t("span",b,[m(e.$slots,"default")]))]))}});export{$ as _,y as a}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/input-native-select.vue_vue_type_script_setup_true_lang-ad2aa845.js: -------------------------------------------------------------------------------- 1 | import{d as c,S as p,i as m,p as f,X as v,o,h as r,f as b,t as d,F as g,q as h,G as z,u as S}from"./app-e2c1adb4.js";import{u as V}from"./use-input-size-6b6f86f1.js";const _={value:"",selected:""},k=["value"],y=c({__name:"input-native-select",props:p({items:{},size:{},error:{},placeholder:{}},{modelValue:{}}),emits:["update:modelValue"],setup(l){const n=l,t=m(l,"modelValue"),{sizeClass:u}=V(n.size??"lg");return(s,a)=>f((o(),r("select",{"onUpdate:modelValue":a[0]||(a[0]=e=>t.value=e),class:z(["w-full rounded-md border border-gray-300 px-3 text-left shadow-sm focus:border-blue-500 focus:ring-blue-500",[S(u),{"border-red-300 text-red-900 placeholder:text-red-300 focus:border-red-500 focus:ring-red-500":s.error}]])},[b("option",_,d(s.placeholder??"Select an option"),1),(o(!0),r(g,null,h(s.items,(e,i)=>(o(),r("option",{key:i,value:e.value},d(e.label),9,k))),128))],2)),[[v,t.value]])}});export{y as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js: -------------------------------------------------------------------------------- 1 | import{d,S as i,r as p,i as c,y as f,$ as m,p as y,a0 as v,o as x,h as b,G as g,u as h}from"./app-e2c1adb4.js";import{u as z}from"./use-input-size-6b6f86f1.js";const k=["type"],_=d({__name:"input-text",props:i({type:{default:"text"},size:{default:"lg"},error:{default:""}},{modelValue:{}}),emits:["update:modelValue"],setup(s,{expose:a}){const u=s,t=p(null),r=c(s,"modelValue"),{sizeClass:l}=z(u.size);return f(()=>{m(()=>{var e,o;(e=t.value)!=null&&e.hasAttribute("autofocus")&&((o=t.value)==null||o.focus())})}),a({focus:()=>{var e;return(e=t.value)==null?void 0:e.focus()}}),(e,o)=>y((x(),b("input",{ref_key:"input",ref:t,"onUpdate:modelValue":o[0]||(o[0]=n=>r.value=n),type:e.type,class:g(["w-full rounded-md border-gray-300 placeholder:text-sm placeholder:text-gray-400 focus:border-blue-500 focus:ring-blue-500",[h(l),{"border-red-300 text-red-900 placeholder:text-red-300 focus:border-red-500 focus:ring-red-500":e.error}]])},null,10,k)),[[v,r.value]])}});export{_}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/invited-item-e0f423b9.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./invited-item.vue_vue_type_script_setup_true_lang-365068e6.js";import"./use-confirmation-dialog-9a866ad9.js";import"./dialog-e6df4d57.js";import"./transition-314b73c3.js";import"./app-e2c1adb4.js";import"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./icon-trash-a2f124e2.js";export{o as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/invited-item.vue_vue_type_script_setup_true_lang-365068e6.js: -------------------------------------------------------------------------------- 1 | import{u as y,_ as b}from"./use-confirmation-dialog-9a866ad9.js";import{_ as D}from"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import{_ as C}from"./icon-trash-a2f124e2.js";import{d as k,n as B,o as _,h as m,f as t,t as o,p as j,u as s,b as n,w as l,a,W as z}from"./app-e2c1adb4.js";const T={class:"w-full hover:bg-gray-100"},N={class:"flex h-14 w-full divide-x"},V={class:"grid w-full grid-cols-3 divide-x"},$={class:"col-span-3 flex w-full items-center justify-start px-4 sm:col-span-1"},A={class:"text-sm font-medium text-gray-500"},I={class:"hidden w-full items-center justify-start px-4 md:flex"},W={class:"truncate whitespace-nowrap text-sm font-medium text-gray-500"},E={class:"hidden w-full items-center justify-start px-4 md:flex"},S={class:"truncate whitespace-nowrap text-sm font-medium text-gray-500"},q={class:"grid w-16"},F={class:"flex flex-col p-6"},G=t("span",{class:"text-xl font-medium text-gray-700"},"Are you sure?",-1),H={class:"mt-2 text-sm text-gray-500"},J={class:"font-medium"},K={class:"mt-4 flex gap-4"},R=k({__name:"invited-item",props:{invitation:{}},setup(L){const{loading:p,showDialog:u,openDialog:c,performAction:f,closeDialog:v}=y(),h=async e=>{await f(()=>z.delete(route("ltu.contributors.invite.delete",e)))};return(e,i)=>{const g=C,r=D,x=b,w=B("tooltip");return _(),m("div",T,[t("div",N,[t("div",V,[t("div",$,[t("span",A,o(e.invitation.email),1)]),t("div",I,[t("div",W,o(e.invitation.role.label),1)]),t("div",E,[t("div",S,o(e.invitation.invited_at),1)])]),t("div",q,[j((_(),m("button",{type:"button",class:"group flex items-center justify-center px-3 hover:bg-red-50",onClick:i[0]||(i[0]=(...d)=>s(c)&&s(c)(...d))},[n(g,{class:"size-5 text-gray-400 group-hover:text-red-600"})])),[[w,"Delete"]])]),n(x,{size:"sm",show:s(u)},{default:l(()=>[t("div",F,[G,t("span",H,[a(" This action cannot be undone, This will permanently delete the "),t("span",J,o(e.invitation.email),1),a(" invitation. ")]),t("div",K,[n(r,{variant:"secondary",type:"button",size:"lg","full-width":"",onClick:s(v)},{default:l(()=>[a(" Cancel ")]),_:1},8,["onClick"]),n(r,{variant:"danger",type:"button",size:"lg","is-loading":s(p),"full-width":"",onClick:i[1]||(i[1]=d=>h(e.invitation.id))},{default:l(()=>[a(" Delete ")]),_:1},8,["is-loading"])])])]),_:1},8,["show"])])])}}});export{R as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/invited-table-64fbaaaa.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./invited-table.vue_vue_type_script_setup_true_lang-ac2c147c.js";import"./pagination.vue_vue_type_script_setup_true_lang-8ed7a73a.js";import"./icon-arrow-right-e09bb615.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./app-e2c1adb4.js";import"./icon-arrow-left-b70428ec.js";import"./icon-plus-4cbbfd59.js";import"./invited-item.vue_vue_type_script_setup_true_lang-365068e6.js";import"./use-confirmation-dialog-9a866ad9.js";import"./dialog-e6df4d57.js";import"./transition-314b73c3.js";import"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import"./icon-trash-a2f124e2.js";export{o as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/invited-table.vue_vue_type_script_setup_true_lang-ac2c147c.js: -------------------------------------------------------------------------------- 1 | import{_ as m}from"./pagination.vue_vue_type_script_setup_true_lang-8ed7a73a.js";import{d as _,n as p,o as e,h as i,f as s,p as u,c as a,w as f,b as o,F as v,q as h,I as x,P as g}from"./app-e2c1adb4.js";import{_ as y}from"./icon-plus-4cbbfd59.js";import{_ as w}from"./invited-item.vue_vue_type_script_setup_true_lang-365068e6.js";const k={class:"w-full divide-y overflow-hidden rounded-md bg-white shadow"},b={class:"w-full shadow-md"},j={class:"flex h-14 w-full divide-x"},B=x('
Email
',1),I={class:"grid w-16"},P={key:1,class:"flex w-full items-center justify-center bg-gray-50 px-4 py-12"},C=s("span",{class:"text-sm text-gray-400"},"There are no invitations yet..",-1),D=[C],q=_({__name:"invited-table",props:{invitations:{}},setup(N){return(t,V)=>{const l=y,r=g,d=m,c=p("tooltip");return e(),i("div",k,[s("div",b,[s("div",j,[B,s("div",I,[u((e(),a(r,{href:t.route("ltu.contributors.invite"),class:"group flex items-center justify-center px-3 hover:bg-blue-50"},{default:f(()=>[o(l,{class:"size-5 text-gray-400 group-hover:text-blue-600"})]),_:1},8,["href"])),[[c,"Invite Contributor"]])])])]),t.invitations.data.length?(e(!0),i(v,{key:0},h(t.invitations.data,n=>(e(),a(w,{key:n.id,invitation:n},null,8,["invitation"]))),128)):(e(),i("div",P,D)),o(d,{links:t.invitations.links,meta:t.invitations.meta},null,8,["links","meta"])])}}});export{q as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/layout-guest-bd2cb4f5.js: -------------------------------------------------------------------------------- 1 | import{o,h as l,b as n,w as r,f as t,k as s,a as i,e as _,P as f}from"./app-e2c1adb4.js";import{_ as m}from"./logo-e09fccf9.js";import{_ as d}from"./_plugin-vue_export-helper-c27b6911.js";const x={},u={class:"flex min-h-screen flex-col items-center justify-center bg-white px-4"},h={class:"flex w-full max-w-xs flex-col gap-y-2"},p={class:"flex flex-col items-center"},g={class:"mt-8 text-xl font-bold text-gray-700"},w={key:0,class:"w-full text-center"},b={class:"mt-2 text-sm leading-6 text-gray-500"},k={class:"mt-6 w-full"};function y(e,v){const a=m,c=f;return o(),l("div",u,[n(c,{href:e.route("ltu.translation.index"),class:"flex h-10 max-w-max"},{default:r(()=>[n(a,{class:"h-10 w-auto fill-current text-gray-500"})]),_:1},8,["href"]),t("div",h,[t("div",p,[t("h1",g,[s(e.$slots,"title",{},()=>[i("Welcome back!")])]),e.$slots.subtitle?(o(),l("div",w,[t("p",b,[s(e.$slots,"subtitle")])])):_("",!0),t("div",k,[s(e.$slots,"default")])])])])}const B=d(x,[["render",y]]);export{B as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/login-9a64952c.js: -------------------------------------------------------------------------------- 1 | import{_ as h}from"./layout-guest-bd2cb4f5.js";import{d as b,C as V,o as p,c as v,w as a,a as m,b as s,h as x,t as B,e as k,f as r,u as o,g as C,m as P,P as $}from"./app-e2c1adb4.js";import{_ as E}from"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import{_ as I}from"./input-password.vue_vue_type_script_setup_true_lang-9dd0bcca.js";import{_ as L,a as N}from"./input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js";import{_ as q}from"./input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js";import"./logo-e09fccf9.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./icon-close-5cb91d75.js";import"./use-input-size-6b6f86f1.js";const S={key:0,class:"mb-4 text-sm font-medium text-green-600"},F={class:"space-y-1"},T={class:"space-y-1"},U={class:"mt-8 flex w-full justify-center"},R=b({__name:"login",props:{status:{}},setup(j){const e=V({email:"",password:"",remember:!1}),d=()=>{e.post(route("ltu.login.attempt"),{onFinish:()=>{e.reset("password")}})};return(n,t)=>{const c=P,i=L,_=q,u=N,f=I,g=E,w=$,y=h;return p(),v(y,null,{title:a(()=>[m(" Sign in to your account")]),default:a(()=>[s(c,{title:"Log in"}),n.status?(p(),x("div",S,B(n.status),1)):k("",!0),r("form",{class:"space-y-6",onSubmit:C(d,["prevent"])},[r("div",F,[s(i,{for:"email",value:"Email Address",class:"sr-only"}),s(_,{id:"email",modelValue:o(e).email,"onUpdate:modelValue":t[0]||(t[0]=l=>o(e).email=l),error:o(e).errors.email,type:"email",required:"",autofocus:"",placeholder:"Email address",autocomplete:"username",class:"bg-gray-50"},null,8,["modelValue","error"]),s(u,{message:o(e).errors.email},null,8,["message"])]),r("div",T,[s(i,{for:"password",value:"Password",class:"sr-only"}),s(f,{id:"password",modelValue:o(e).password,"onUpdate:modelValue":t[1]||(t[1]=l=>o(e).password=l),error:o(e).errors.password,required:"",autocomplete:"current-password",placeholder:"Password",class:"bg-gray-50"},null,8,["modelValue","error"]),s(u,{message:o(e).errors.password},null,8,["message"])]),s(g,{type:"submit",size:"lg",variant:"secondary","is-loading":o(e).processing,"full-width":""},{default:a(()=>[m(" Continue ")]),_:1},8,["is-loading"])],32),r("div",U,[s(w,{href:n.route("ltu.password.request"),class:"text-xs font-medium text-gray-500 hover:text-blue-500"},{default:a(()=>[m(" Forgot your password? ")]),_:1},8,["href"])])]),_:1})}}});export{R as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/logo-e09fccf9.js: -------------------------------------------------------------------------------- 1 | import{_ as c}from"./_plugin-vue_export-helper-c27b6911.js";import{o as t,h as o,I as s}from"./app-e2c1adb4.js";const h={},a={xmlns:"http://www.w3.org/2000/svg",fill:"currentColor",viewBox:"0 0 700 700"},e=s('',6),p=[e];function _(n,r){return t(),o("svg",a,p)}const i=c(h,[["render",_]]);export{i as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/pagination.vue_vue_type_script_setup_true_lang-8ed7a73a.js: -------------------------------------------------------------------------------- 1 | import{_ as i}from"./icon-arrow-right-e09bb615.js";import{d as _,o as t,h as o,c as l,w as c,b as s,f as u,t as p,P as m}from"./app-e2c1adb4.js";import{_ as d}from"./icon-arrow-left-b70428ec.js";const f={class:"group flex w-full items-center justify-between gap-6 p-4"},g={key:1,type:"button",class:"cursor-not-allowed",disabled:""},h={class:"text-sm font-medium text-gray-500 group-hover:text-blue-600"},k={key:3,type:"button",class:"cursor-not-allowed",disabled:""},z=_({__name:"pagination",props:{links:{},meta:{}},setup(y){return(e,b)=>{const n=d,r=m,a=i;return t(),o("div",f,[e.links.prev?(t(),l(r,{key:0,href:e.links.prev,"preserve-scroll":""},{default:c(()=>[s(n,{class:"size-6 text-gray-400 group-hover:text-blue-600"})]),_:1},8,["href"])):(t(),o("button",g,[s(n,{class:"size-6 text-gray-400"})])),u("span",h,p(e.meta.current_page)+" of "+p(e.meta.last_page),1),e.links.next?(t(),l(r,{key:2,href:e.links.next,"preserve-scroll":""},{default:c(()=>[s(a,{class:"size-6 text-gray-400 group-hover:text-blue-600"})]),_:1},8,["href"])):(t(),o("button",k,[s(a,{class:"size-6 text-gray-400"})]))])}}});export{z as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/phrase-item-90a67d96.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./phrase-item.vue_vue_type_script_setup_true_lang-6c611958.js";import"./icon-pencil-8922e06b.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./app-e2c1adb4.js";import"./icon-language-56d3f627.js";export{o as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/phrase-item.vue_vue_type_script_setup_true_lang-6c611958.js: -------------------------------------------------------------------------------- 1 | import{_ as m}from"./icon-pencil-8922e06b.js";import{o as s,h as c,f as t,d as f,n as g,G as v,c as o,b as a,w as l,t as r,p as w,P as x}from"./app-e2c1adb4.js";import{_ as y}from"./icon-language-56d3f627.js";import{_ as b}from"./_plugin-vue_export-helper-c27b6911.js";const k={},z={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},j=t("path",{d:"M0 0h24v24H0z",fill:"none"},null,-1),B=t("path",{d:"M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"},null,-1),C=[j,B];function L(d,e){return s(),c("svg",z,C)}const $=b(k,[["render",L]]),D={class:"w-full hover:bg-gray-100"},I={class:"flex h-14 w-full divide-x"},P={class:"flex w-full items-center justify-start px-4"},E={class:"truncate rounded-md border bg-white px-1.5 py-0.5 text-sm font-medium text-gray-600 hover:border-blue-400 hover:bg-blue-50 hover:text-blue-600"},M={class:"hidden w-full items-center justify-start px-4 md:flex"},N={class:"truncate whitespace-nowrap text-sm font-medium text-gray-400"},V={class:"flex w-full items-center justify-start px-4"},G={class:"w-full truncate whitespace-nowrap text-sm font-medium text-gray-600"},H={class:"grid w-[67px] grid-cols-1 divide-x"},K=f({__name:"phrase-item",props:{phrase:{},translation:{}},setup(d){return(e,S)=>{const p=$,_=y,n=x,h=m,u=g("tooltip");return s(),c("div",D,[t("div",I,[t("div",{class:v(["hidden w-20 items-center justify-center px-4 md:flex",{"bg-green-50":e.phrase.state,"hover:bg-green-100":e.phrase.state}])},[e.phrase.state?(s(),o(p,{key:0,class:"size-5 text-green-600"})):(s(),o(_,{key:1,class:"size-5 text-gray-500"}))],2),a(n,{href:e.route("ltu.phrases.edit",{translation:e.translation.id,phrase:e.phrase.uuid}),class:"grid w-full grid-cols-2 divide-x md:grid-cols-3"},{default:l(()=>{var i;return[t("div",P,[t("div",E,r(e.phrase.key),1)]),t("div",M,[t("div",N,r((i=e.phrase.source)==null?void 0:i.value),1)]),t("div",V,[t("div",G,r(e.phrase.value),1)])]}),_:1},8,["href"]),t("div",H,[w((s(),o(n,{href:e.route("ltu.phrases.edit",{translation:e.translation.id,phrase:e.phrase.uuid}),class:"group flex items-center justify-center px-3 hover:bg-blue-50"},{default:l(()=>[a(h,{class:"size-5 text-gray-400 group-hover:text-blue-600"})]),_:1},8,["href"])),[[u,"Edit"]])])])])}}});export{K as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/reset-password-12bd1497.js: -------------------------------------------------------------------------------- 1 | import{_ as y}from"./layout-guest-bd2cb4f5.js";import{_ as b}from"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import{_ as v}from"./input-password.vue_vue_type_script_setup_true_lang-9dd0bcca.js";import{_ as V,a as h}from"./input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js";import{d as P,C as x,o as C,c as k,w as t,a as d,b as o,f as r,t as B,u as e,g as N,m as $}from"./app-e2c1adb4.js";import"./logo-e09fccf9.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js";import"./use-input-size-6b6f86f1.js";import"./icon-close-5cb91d75.js";const E={class:"space-y-1 opacity-45"},I=["textContent"],R={class:"space-y-1"},q={class:"space-y-1"},j=P({__name:"reset-password",props:{email:{},token:{}},setup(c){const m=c,s=x({token:m.token,email:m.email,password:"",password_confirmation:""}),u=()=>{s.post(route("ltu.password.update"),{onFinish:()=>{s.reset("password","password_confirmation")}})};return(_,a)=>{const w=$,n=V,i=h,p=v,f=b,g=y;return C(),k(g,null,{title:t(()=>[d(" Reset Password ")]),subtitle:t(()=>[d(" Ensure your new password is at least 8 characters long to ensure security. ")]),default:t(()=>[o(w,{title:"Reset Password"}),r("form",{class:"space-y-6",onSubmit:N(u,["prevent"])},[r("div",E,[o(n,{for:"email",value:"Email Address",class:"sr-only"}),r("div",{class:"w-full cursor-not-allowed rounded-md border border-gray-300 bg-gray-100 px-2 py-2.5 text-sm",textContent:B(_.email)},null,8,I),o(i,{message:e(s).errors.email},null,8,["message"])]),r("div",R,[o(n,{for:"password",value:"Password",class:"sr-only"}),o(p,{id:"password",modelValue:e(s).password,"onUpdate:modelValue":a[0]||(a[0]=l=>e(s).password=l),error:e(s).errors.password,required:"",autocomplete:"new-password",placeholder:"New Password",class:"bg-gray-50"},null,8,["modelValue","error"]),o(i,{message:e(s).errors.password},null,8,["message"])]),r("div",q,[o(n,{for:"password_confirmation",value:"Confirm Password",class:"sr-only"}),o(p,{id:"password_confirmation",modelValue:e(s).password_confirmation,"onUpdate:modelValue":a[1]||(a[1]=l=>e(s).password_confirmation=l),error:e(s).errors.password_confirmation,required:"",autocomplete:"new-password",placeholder:"Confirm Password",class:"bg-gray-50"},null,8,["modelValue","error"]),o(i,{message:e(s).errors.password_confirmation},null,8,["message"])]),o(f,{type:"submit",variant:"secondary","is-loading":e(s).processing,"full-width":""},{default:t(()=>[d(" Reset Password ")]),_:1},8,["is-loading"])],32)]),_:1})}}});export{j as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/source-phrase-item-34b7c08a.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./source-phrase-item.vue_vue_type_script_setup_true_lang-971d7c33.js";import"./use-confirmation-dialog-9a866ad9.js";import"./dialog-e6df4d57.js";import"./transition-314b73c3.js";import"./app-e2c1adb4.js";import"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./icon-trash-a2f124e2.js";import"./icon-pencil-8922e06b.js";import"./input-checkbox.vue_vue_type_script_setup_true_lang-cbb48805.js";export{o as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/translation-item-27ea6c78.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./translation-item.vue_vue_type_script_setup_true_lang-f94e4148.js";import"./use-confirmation-dialog-9a866ad9.js";import"./dialog-e6df4d57.js";import"./transition-314b73c3.js";import"./app-e2c1adb4.js";import"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./icon-trash-a2f124e2.js";import"./icon-language-56d3f627.js";import"./flag.vue_vue_type_script_setup_true_lang-9a70fac7.js";import"./input-checkbox.vue_vue_type_script_setup_true_lang-cbb48805.js";export{o as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/use-auth-46901eba.js: -------------------------------------------------------------------------------- 1 | import{v as r,_ as s}from"./app-e2c1adb4.js";const u=()=>r(()=>s().props.auth.user);export{u}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/use-confirmation-dialog-9a866ad9.js: -------------------------------------------------------------------------------- 1 | import{G as p,U as w}from"./dialog-e6df4d57.js";import{d as v,D as y,y as h,v as x,o as g,c as _,w as n,b as c,u as o,f,G as b,k,a5 as C,r as d,a6 as S}from"./app-e2c1adb4.js";import{k as m,S as B}from"./transition-314b73c3.js";const D=f("div",{class:"fixed inset-0 bg-gray-500/25 backdrop-blur transition-opacity"},null,-1),E={class:"fixed inset-0 overflow-y-auto"},T={class:"flex min-h-full items-center justify-center p-4 text-center"},A=v({__name:"confirmation-dialog",props:{size:{default:"lg"},closeable:{type:Boolean,default:!0},show:{type:Boolean,default:!1}},emits:["close"],setup(u,{emit:t}){const a=u,r=t;y(()=>a.show,e=>{e&&r("close")});const l=()=>{a.closeable&&r("close")};h(()=>document.addEventListener("keydown",e=>e.key==="Escape"&&l()));const i=x(()=>({sm:"sm:max-w-sm",md:"sm:max-w-md",lg:"sm:max-w-lg",xl:"sm:max-w-xl","2xl":"sm:max-w-2xl"})[a.size]);return(e,s)=>(g(),_(o(B),{appear:"",as:"template",show:e.show},{default:n(()=>[c(o(w),{as:"div",class:"relative z-50",onClose:l},{default:n(()=>[c(o(m),{as:"template","leave-to":"opacity-0",enter:"duration-300 ease-out","enter-from":"opacity-0","enter-to":"opacity-100",leave:"duration-200 ease-in","leave-from":"opacity-100",onAfterLeave:l},{default:n(()=>[D]),_:1}),f("div",E,[f("div",T,[c(o(m),{as:"template",enter:"duration-300 ease-out","enter-from":"opacity-0 scale-95","enter-to":"opacity-100 scale-100",leave:"duration-200 ease-in","leave-from":"opacity-100 scale-100","leave-to":"opacity-0 scale-95"},{default:n(()=>[c(o(p),{class:b([[o(i)],"w-full rounded-2xl bg-white text-left align-middle shadow-xl transition-all"])},{default:n(()=>[k(e.$slots,"default")]),_:3},8,["class"])]),_:3})])])]),_:3})]),_:3},8,["show"]))}});function I(){const u=C(),t=d(!1),a=d(!1),r=()=>{a.value=!0};async function l(e,s){t.value=!0;try{e(),t.value=!1,i(),s&&s.onSuccess&&s.onSuccess()}catch{u.error("Something went wrong, please try again.",{icon:!0,position:S.BOTTOM_CENTER}),t.value=!1,s&&s.onError&&s.onError()}}const i=()=>{a.value=!1};return{loading:t,showDialog:a,openDialog:r,performAction:l,closeDialog:i}}export{A as _,I as u}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/use-input-size-6b6f86f1.js: -------------------------------------------------------------------------------- 1 | import{v as t}from"./app-e2c1adb4.js";const p=s=>({sizeClass:t(()=>({xs:"py-1 text-sm",sm:"py-1.5 text-sm",md:"py-2 text-sm",lg:"py-2.5 text-sm"})[s])});export{p as u}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/user-update-password-form-0700c2a6.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./user-update-password-form.vue_vue_type_script_setup_true_lang-7b0268c2.js";import"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./app-e2c1adb4.js";import"./input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js";import"./icon-close-5cb91d75.js";import"./input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js";import"./use-input-size-6b6f86f1.js";export{o as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/user-update-password-form.vue_vue_type_script_setup_true_lang-7b0268c2.js: -------------------------------------------------------------------------------- 1 | import{_ as v}from"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import{_ as g,a as V}from"./input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js";import{_ as x}from"./input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js";import{d as I,r as i,C as P,o as m,h as _,f as a,b as r,u as e,w,a as h,e as C,T as S,g as k}from"./app-e2c1adb4.js";const B=a("header",null,[a("h2",{class:"text-lg font-medium text-gray-900"},"Update Password"),a("p",{class:"mt-1 text-sm text-gray-600"},"Ensure your account is using a long, random password to stay secure.")],-1),N={class:"space-y-1"},b={class:"space-y-1"},E={class:"space-y-1"},T={class:"flex items-center gap-4"},U={key:0,class:"text-sm text-gray-600"},q=I({__name:"user-update-password-form",setup($){const c=i(null),l=i(null),s=P({current_password:"",password:"",password_confirmation:""}),f=()=>{s.put(route("ltu.profile.password.update"),{preserveScroll:!0,onSuccess:()=>{s.reset()},onError:()=>{var n,o;s.errors.password&&(s.reset("password","password_confirmation"),(n=c.value)==null||n.focus()),s.errors.current_password&&(s.reset("current_password"),(o=l.value)==null||o.focus())}})};return(n,o)=>{const d=g,p=x,u=V,y=v;return m(),_("section",null,[B,a("form",{class:"mt-6 space-y-6",onSubmit:k(f,["prevent"])},[a("div",N,[r(d,{for:"current_password",value:"Current Password"}),r(p,{id:"current_password",ref_key:"currentPasswordInput",ref:l,modelValue:e(s).current_password,"onUpdate:modelValue":o[0]||(o[0]=t=>e(s).current_password=t),error:e(s).errors.current_password,type:"password",autocomplete:"current-password"},null,8,["modelValue","error"]),r(u,{message:e(s).errors.current_password},null,8,["message"])]),a("div",b,[r(d,{for:"password",value:"New Password"}),r(p,{id:"password",ref_key:"passwordInput",ref:c,modelValue:e(s).password,"onUpdate:modelValue":o[1]||(o[1]=t=>e(s).password=t),error:e(s).errors.password,type:"password",autocomplete:"new-password"},null,8,["modelValue","error"]),r(u,{message:e(s).errors.password},null,8,["message"])]),a("div",E,[r(d,{for:"password_confirmation",value:"Confirm Password"}),r(p,{id:"password_confirmation",modelValue:e(s).password_confirmation,"onUpdate:modelValue":o[2]||(o[2]=t=>e(s).password_confirmation=t),type:"password",autocomplete:"new-password"},null,8,["modelValue"]),r(u,{message:e(s).errors.password_confirmation},null,8,["message"])]),a("div",T,[r(y,{type:"submit",size:"md",variant:"primary","is-loading":e(s).processing},{default:w(()=>[h(" Save ")]),_:1},8,["is-loading"]),r(S,{"enter-active-class":"transition ease-in-out","enter-from-class":"opacity-0","leave-active-class":"transition ease-in-out","leave-to-class":"opacity-0"},{default:w(()=>[e(s).recentlySuccessful?(m(),_("p",U,"Saved.")):C("",!0)]),_:1})])],32)])}}});export{q as _}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/user-update-profile-information-form-36922b8f.js: -------------------------------------------------------------------------------- 1 | import{_ as o}from"./user-update-profile-information-form.vue_vue_type_script_setup_true_lang-aa37d789.js";import"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import"./_plugin-vue_export-helper-c27b6911.js";import"./app-e2c1adb4.js";import"./input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js";import"./icon-close-5cb91d75.js";import"./input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js";import"./use-input-size-6b6f86f1.js";import"./use-auth-46901eba.js";export{o as default}; 2 | -------------------------------------------------------------------------------- /resources/dist/vendor/translations-ui/assets/user-update-profile-information-form.vue_vue_type_script_setup_true_lang-aa37d789.js: -------------------------------------------------------------------------------- 1 | import{_}from"./base-button.vue_vue_type_script_setup_true_lang-4041aa1c.js";import{_ as y,a as v}from"./input-label.vue_vue_type_script_setup_true_lang-d2d25eb9.js";import{_ as g}from"./input-text.vue_vue_type_script_setup_true_lang-09f5c91a.js";import{d as x,C as V,o as c,h as u,f as o,b as t,u as a,w as p,a as h,e as B,T as I,g as N}from"./app-e2c1adb4.js";import{u as S}from"./use-auth-46901eba.js";const b=o("header",null,[o("h2",{class:"text-lg font-medium text-gray-900"},"Profile Information"),o("p",{class:"mt-1 text-sm text-gray-600"},"Update your account's profile information and email address.")],-1),C={class:"space-y-1"},E={class:"space-y-1"},T={class:"flex items-center gap-4"},$={key:0,class:"text-sm text-gray-600"},L=x({__name:"user-update-profile-information-form",props:{mustVerifyEmail:{},status:{}},setup(k){const n=S().value,e=V({name:n.name,email:n.email}),d=()=>{e.patch(route("ltu.profile.update"),{preserveScroll:!0})};return(w,s)=>{const m=y,i=g,l=v,f=_;return c(),u("section",null,[b,o("form",{class:"mt-6 space-y-6",onSubmit:N(d,["prevent"])},[o("div",C,[t(m,{for:"name",value:"Name"}),t(i,{id:"name",modelValue:a(e).name,"onUpdate:modelValue":s[0]||(s[0]=r=>a(e).name=r),error:a(e).errors.name,type:"text",required:"",autofocus:"",autocomplete:"name"},null,8,["modelValue","error"]),t(l,{message:a(e).errors.name},null,8,["message"])]),o("div",E,[t(m,{for:"email",value:"Email"}),t(i,{id:"email",modelValue:a(e).email,"onUpdate:modelValue":s[1]||(s[1]=r=>a(e).email=r),error:a(e).errors.email,type:"email",autocomplete:"username"},null,8,["modelValue","error"]),t(l,{message:a(e).errors.email},null,8,["message"])]),o("div",T,[t(f,{type:"submit",size:"md",variant:"primary","is-loading":a(e).processing},{default:p(()=>[h(" Save ")]),_:1},8,["is-loading"]),t(I,{"enter-active-class":"transition ease-in-out","enter-from-class":"opacity-0","leave-active-class":"transition ease-in-out","leave-to-class":"opacity-0"},{default:p(()=>[a(e).recentlySuccessful?(c(),u("p",$,"Saved.")):B("",!0)]),_:1})])],32)])}}});export{L as _}; 2 | -------------------------------------------------------------------------------- /resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohmmedAshraf/laravel-translations/ae8a53499e5f02bef1180afaaa4e402a57739826/resources/favicon.ico -------------------------------------------------------------------------------- /resources/scripts/app.ts: -------------------------------------------------------------------------------- 1 | import "../css/app.scss" 2 | import { createApp, h, DefineComponent } from "vue" 3 | import { VTooltip } from "floating-vue" 4 | import { modal } from "momentum-modal" 5 | import "floating-vue/dist/style.css" 6 | import Toast from "vue-toastification" 7 | import "vue-select/dist/vue-select.css" 8 | import { createInertiaApp } from "@inertiajs/vue3" 9 | import { Tabs, Tab } from "vue3-tabs-component" 10 | import { ZiggyVue } from "ziggy-js" 11 | import { notifications } from "./plugins/notifications" 12 | import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers" 13 | import "vue-toastification/dist/index.css" 14 | 15 | const appName = import.meta.env.VITE_APP_NAME || "Laravel" 16 | 17 | createInertiaApp({ 18 | title: (title) => `${title} - ${appName}`, 19 | resolve: (name: string) => { 20 | return resolvePageComponent(`../views/pages/${name}.vue`, import.meta.glob("../views/pages/**/*.vue")) 21 | }, 22 | setup({ el, App, props, plugin }) { 23 | createApp({ render: () => h(App, props) }) 24 | .directive("tooltip", VTooltip) 25 | .use(modal, { 26 | resolve: (name: string) => { 27 | return resolvePageComponent(`../views/pages/${name}.vue`, import.meta.glob("../views/pages/**/*.vue")) 28 | }, 29 | }) 30 | .use(Toast, { 31 | transition: "Vue-Toastification__bounce", 32 | maxToasts: 20, 33 | newestOnTop: true, 34 | }) 35 | .use(notifications) 36 | .use(plugin) 37 | .use(ZiggyVue) 38 | .component("tabs", Tabs) 39 | .component("tab", Tab) 40 | .mount(el) 41 | }, 42 | progress: { 43 | color: "#4B5563", 44 | }, 45 | }).then(() => { 46 | // ... 47 | }) 48 | -------------------------------------------------------------------------------- /resources/scripts/composables/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohmmedAshraf/laravel-translations/ae8a53499e5f02bef1180afaaa4e402a57739826/resources/scripts/composables/.gitkeep -------------------------------------------------------------------------------- /resources/scripts/composables/use-auth.ts: -------------------------------------------------------------------------------- 1 | import { Contributor } from "../types" 2 | 3 | export const useAuth = () => { 4 | return computed(() => usePage().props.auth.user as Contributor) 5 | } 6 | -------------------------------------------------------------------------------- /resources/scripts/composables/use-button-size.ts: -------------------------------------------------------------------------------- 1 | import { computed, ComputedRef } from "vue" 2 | 3 | type ButtonSize = "xs" | "sm" | "md" | "lg" 4 | 5 | const useButtonSize = (size: ButtonSize): { sizeClass: ComputedRef } => { 6 | const sizeClass = computed(() => { 7 | return { 8 | xs: "btn-xs", 9 | sm: "btn-sm", 10 | md: "btn-md", 11 | lg: "btn-lg", 12 | }[size] 13 | }) 14 | 15 | return { 16 | sizeClass, 17 | } 18 | } 19 | 20 | export default useButtonSize 21 | -------------------------------------------------------------------------------- /resources/scripts/composables/use-button-variant.ts: -------------------------------------------------------------------------------- 1 | import { computed, ComputedRef } from "vue" 2 | 3 | type ButtonVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "dark" 4 | 5 | const useButtonVariant = (variant: ButtonVariant): { variantClass: ComputedRef } => { 6 | const variantClass = computed(() => { 7 | return { 8 | primary: "btn-primary", 9 | secondary: "btn-secondary", 10 | success: "btn-success", 11 | danger: "btn-danger", 12 | warning: "btn-warning", 13 | dark: "btn-dark", 14 | }[variant] 15 | }) 16 | 17 | return { 18 | variantClass, 19 | } 20 | } 21 | 22 | export default useButtonVariant 23 | -------------------------------------------------------------------------------- /resources/scripts/composables/use-confirmation-dialog.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue" 2 | import { POSITION, useToast } from "vue-toastification" 3 | 4 | export default function useConfirmationDialog() { 5 | const toast = useToast() 6 | const loading = ref(false) 7 | const showDialog = ref(false) 8 | 9 | const openDialog = () => { 10 | showDialog.value = true 11 | } 12 | 13 | async function performAction(actionFunction: () => void, options?: { onSuccess?: () => void; onError?: () => void }) { 14 | loading.value = true 15 | 16 | try { 17 | actionFunction() 18 | 19 | loading.value = false 20 | closeDialog() 21 | 22 | if (options && options.onSuccess) { 23 | options.onSuccess() 24 | } 25 | } catch (error) { 26 | toast.error("Something went wrong, please try again.", { 27 | icon: true, 28 | position: POSITION.BOTTOM_CENTER, 29 | }) 30 | loading.value = false 31 | if (options && options.onError) { 32 | options.onError() 33 | } 34 | } 35 | } 36 | 37 | const closeDialog = () => { 38 | showDialog.value = false 39 | } 40 | 41 | return { 42 | loading, 43 | showDialog, 44 | openDialog, 45 | performAction, 46 | closeDialog, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /resources/scripts/composables/use-input-size.ts: -------------------------------------------------------------------------------- 1 | import { computed, ComputedRef } from "vue" 2 | 3 | type ButtonSize = "xs" | "sm" | "md" | "lg" 4 | 5 | const useInputSize = (size: ButtonSize): { sizeClass: ComputedRef } => { 6 | const sizeClass = computed(() => { 7 | return { 8 | xs: "py-1 text-sm", 9 | sm: "py-1.5 text-sm", 10 | md: "py-2 text-sm", 11 | lg: "py-2.5 text-sm", 12 | }[size] 13 | }) 14 | 15 | return { 16 | sizeClass, 17 | } 18 | } 19 | 20 | export default useInputSize 21 | -------------------------------------------------------------------------------- /resources/scripts/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohmmedAshraf/laravel-translations/ae8a53499e5f02bef1180afaaa4e402a57739826/resources/scripts/plugins/.gitkeep -------------------------------------------------------------------------------- /resources/scripts/plugins/notifications.ts: -------------------------------------------------------------------------------- 1 | import "vue-toastification/dist/index.css" 2 | import { Notification } from "../types" 3 | import { POSITION, TYPE as NotificationType, useToast } from "vue-toastification" 4 | 5 | const toast = useToast() 6 | 7 | export const notifications = () => { 8 | router.on("finish", () => { 9 | const notification = usePage().props.notification as Notification | null 10 | 11 | if (notification) { 12 | toast(notification.body, { 13 | position: POSITION.BOTTOM_CENTER, 14 | type: notification.type as NotificationType, 15 | timeout: 5000, 16 | closeOnClick: true, 17 | pauseOnFocusLoss: true, 18 | pauseOnHover: true, 19 | draggable: true, 20 | draggablePercent: 0.6, 21 | showCloseButtonOnHover: false, 22 | hideProgressBar: true, 23 | closeButton: "button", 24 | icon: true, 25 | }) 26 | } 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /resources/scripts/types/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohmmedAshraf/laravel-translations/ae8a53499e5f02bef1180afaaa4e402a57739826/resources/scripts/types/.gitkeep -------------------------------------------------------------------------------- /resources/scripts/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import { PageProps as InertiaPageProps } from '@inertiajs/core'; 2 | import { AxiosInstance } from 'axios'; 3 | import { route } from 'ziggy-js'; 4 | import { PageProps as AppPageProps } from './'; 5 | 6 | declare global { 7 | interface Window { 8 | axios: AxiosInstance; 9 | } 10 | 11 | let route: typeof route 12 | } 13 | 14 | declare module 'vue' { 15 | interface ComponentCustomProperties { 16 | route: typeof route; 17 | } 18 | } 19 | 20 | declare module '@inertiajs/core' { 21 | interface PageProps extends InertiaPageProps, AppPageProps {} 22 | } 23 | -------------------------------------------------------------------------------- /resources/scripts/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export type Language = { 2 | id: number 3 | name: string 4 | code: string 5 | rtl: boolean 6 | } 7 | 8 | export type Role = { 9 | value: string 10 | label: string 11 | } 12 | 13 | export type NotificationType = "success" | "error" | "warning" | "info" | "default" 14 | 15 | export type Notification = { 16 | type: NotificationType 17 | body: string 18 | } 19 | 20 | export type MachineTranslations = { 21 | id: number 22 | value: string 23 | engine: string 24 | } 25 | 26 | export type Contributor = { 27 | id: number 28 | name: string 29 | email: string 30 | role: Role 31 | created_at: string 32 | updated_at: string 33 | } 34 | 35 | export type Invite = { 36 | id: number 37 | email: string 38 | role: Role 39 | invited_at: string 40 | } 41 | 42 | export type SourcePhrase = { 43 | id: number 44 | uuid: string 45 | key: string 46 | group: string 47 | value: string 48 | parameters: Array 49 | created_at: string 50 | updated_at: string 51 | state: boolean 52 | note: string 53 | value_html: Array<{ 54 | parameter: boolean 55 | value: string 56 | }> 57 | translation_id: number 58 | translation_file_id: number 59 | file: TranslationFile 60 | } 61 | 62 | export type Phrase = { 63 | id: number 64 | uuid: string 65 | key: string 66 | group: string 67 | value: any 68 | parameters: Array 69 | created_at: string 70 | updated_at: string 71 | state: boolean 72 | value_html: Array<{ 73 | parameter: boolean 74 | value: string 75 | }> 76 | translation_id: number 77 | translation_file_id: number 78 | phrase_id: number 79 | source: SourcePhrase 80 | file: TranslationFile 81 | } 82 | 83 | export type SourceTranslation = { 84 | id: number 85 | language: Language 86 | progress: number 87 | phrases_count: number 88 | created_at: string 89 | updated_at: string 90 | } 91 | 92 | export type Translation = { 93 | id: number 94 | language: Language 95 | source_translation: SourceTranslation 96 | progress: number 97 | source: boolean 98 | phrases_count: number 99 | created_at: string 100 | updated_at: string 101 | } 102 | 103 | export type TranslationFile = { 104 | id: number 105 | name: string 106 | extension: string 107 | nameWithExtension: string 108 | } 109 | -------------------------------------------------------------------------------- /resources/scripts/utils/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohmmedAshraf/laravel-translations/ae8a53499e5f02bef1180afaaa4e402a57739826/resources/scripts/utils/.gitkeep -------------------------------------------------------------------------------- /resources/scripts/utils/to-items.ts: -------------------------------------------------------------------------------- 1 | export const toItems = (record: Record) => 2 | Object.entries(record).map(([value, label]) => { 3 | return { label, value } 4 | }) 5 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel Translations UI') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @routes 17 | 18 | {{ translationsUIAssets() }} 19 | 20 | @inertiaHead 21 | 22 | 23 | @inertia 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/views/components/alert.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 56 | -------------------------------------------------------------------------------- /resources/views/components/base-button.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 37 | -------------------------------------------------------------------------------- /resources/views/components/confirmation-dialog.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 68 | -------------------------------------------------------------------------------- /resources/views/components/dialog.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 54 | -------------------------------------------------------------------------------- /resources/views/components/empty-state.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /resources/views/components/form/input-checkbox.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /resources/views/components/form/input-error.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /resources/views/components/form/input-file.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /resources/views/components/form/input-label.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /resources/views/components/form/input-multiselect.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 50 | -------------------------------------------------------------------------------- /resources/views/components/form/input-native-select.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | -------------------------------------------------------------------------------- /resources/views/components/form/input-password.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /resources/views/components/form/input-select.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 51 | -------------------------------------------------------------------------------- /resources/views/components/form/input-text.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 45 | -------------------------------------------------------------------------------- /resources/views/components/form/input-textarea.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-arrow-left.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-arrow-right.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-check.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-clipboard.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-close.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-cog.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-document.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-ellipsis-vertical.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-external-link.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-eye-off.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-eye.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-google.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-key.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-language.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-loading.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-mail.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-pencil.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-plus.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-publish.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-similar.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-speak.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-star.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-sync.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-translation.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-trash.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/icon-versions.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/logo.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /resources/views/components/pagination.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | -------------------------------------------------------------------------------- /resources/views/components/phrase/phrase-with-parameters.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 65 | -------------------------------------------------------------------------------- /resources/views/components/phrase/phrases-filter.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 38 | -------------------------------------------------------------------------------- /resources/views/components/phrase/similar/similar-phrases-item.vue: -------------------------------------------------------------------------------- 1 | 8 | 31 | -------------------------------------------------------------------------------- /resources/views/components/phrase/similar/similar-phrases.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /resources/views/components/phrase/suggestions/machine-translate.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 42 | -------------------------------------------------------------------------------- /resources/views/layouts/dashboard/layout-dashboard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 53 | -------------------------------------------------------------------------------- /resources/views/layouts/guest/layout-guest.vue: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /resources/views/mail/invite.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # Hello 3 | 4 | Someone has invited you to join their translation team on {{ config('app.name') }}. 5 | 6 | @component('mail::button', ['url' => $link]) 7 | Accept Invitation 8 | @endcomponent 9 | 10 | This invitation link will expire in 24 hours. 11 | 12 | If you did not request a password reset, no further action is required. 13 | 14 | Thanks,
15 | {{ config('app.name') }} 16 | @endcomponent 17 | -------------------------------------------------------------------------------- /resources/views/mail/password.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # Hello 3 | 4 | You are receiving this email because we received a password reset request for your account. 5 | 6 | @component('mail::button', ['url' => $link]) 7 | Reset Password 8 | @endcomponent 9 | 10 | This password reset link will expire in 60 minutes. 11 | 12 | If you did not request a password reset, no further action is required. 13 | 14 | Thanks,
15 | {{ config('app.name') }} 16 | @endcomponent 17 | -------------------------------------------------------------------------------- /resources/views/pages/auth/forgot-password.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 48 | -------------------------------------------------------------------------------- /resources/views/pages/auth/invite/accept.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 73 | -------------------------------------------------------------------------------- /resources/views/pages/auth/login.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 56 | -------------------------------------------------------------------------------- /resources/views/pages/auth/reset-password.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 60 | -------------------------------------------------------------------------------- /resources/views/pages/contributor/partials/invited-table.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 49 | -------------------------------------------------------------------------------- /resources/views/pages/error.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 45 | -------------------------------------------------------------------------------- /resources/views/pages/phrases/phrase-item.vue: -------------------------------------------------------------------------------- 1 | 9 | 46 | -------------------------------------------------------------------------------- /resources/views/pages/profile/edit.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /resources/views/pages/profile/partials/user-update-profile-information-form.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 56 | -------------------------------------------------------------------------------- /src/Actions/CopyPhrasesFromSourceAction.php: -------------------------------------------------------------------------------- 1 | first(); 13 | 14 | $sourceTranslation->phrases()->with('file')->get()->each(function ($sourcePhrase) use ($translation) { 15 | $file = $sourcePhrase->file; 16 | 17 | if ($file->is_root) { 18 | $file = TranslationFile::firstOrCreate([ 19 | 'is_root' => true, 20 | 'extension' => $file->extension, 21 | 'name' => $translation->language->code, 22 | ]); 23 | } 24 | 25 | $translation->phrases()->create([ 26 | 'value' => null, 27 | 'uuid' => str()->uuid(), 28 | 'key' => $sourcePhrase->key, 29 | 'group' => $file->name, 30 | 'phrase_id' => $sourcePhrase->id, 31 | 'parameters' => $sourcePhrase->parameters, 32 | 'translation_file_id' => $file->id, 33 | ]); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Actions/CopySourceKeyToTranslationsAction.php: -------------------------------------------------------------------------------- 1 | get()->each(function ($translation) use ($sourceKey) { 14 | $isRoot = TranslationFile::find($sourceKey->file->id)?->is_root; 15 | $locale = $translation->language()->first()?->code; 16 | $translation->phrases()->create([ 17 | 'value' => null, 18 | 'uuid' => str()->uuid(), 19 | 'key' => $sourceKey->key, 20 | 'group' => ($isRoot ? $locale : $sourceKey->group), 21 | 'phrase_id' => $sourceKey->id, 22 | 'parameters' => $sourceKey->parameters, 23 | 'translation_file_id' => ($isRoot ? TranslationFile::firstWhere('name', $locale)?->id : $sourceKey->file->id), 24 | ]); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Actions/CreateSourceKeyAction.php: -------------------------------------------------------------------------------- 1 | first(); 13 | 14 | $sourceKey = $sourceTranslation->phrases()->create([ 15 | 'key' => $key, 16 | 'phrase_id' => null, 17 | 'value' => $content, 18 | 'uuid' => str()->uuid(), 19 | 'translation_file_id' => $file, 20 | 'parameters' => getPhraseParameters($content), 21 | 'group' => TranslationFile::find($file)?->name, 22 | ]); 23 | 24 | CopySourceKeyToTranslationsAction::execute($sourceKey); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Actions/CreateTranslationForLanguageAction.php: -------------------------------------------------------------------------------- 1 | false, 14 | 'language_id' => $language->id, 15 | ]); 16 | 17 | CopyPhrasesFromSourceAction::execute($translation); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Actions/SyncPhrasesAction.php: -------------------------------------------------------------------------------- 1 | first(); 18 | 19 | if (! $language) { 20 | exit; 21 | } 22 | 23 | $translation = Translation::firstOrCreate([ 24 | 'language_id' => $language->id, 25 | 'source' => config('translations.source_language') === $locale, 26 | ]); 27 | 28 | $isRoot = $file === $locale.'.json' || $file === $locale.'.php'; 29 | $extension = pathinfo($file, PATHINFO_EXTENSION); 30 | $filePath = str_replace('.'.$extension, '', preg_replace('/^'.preg_quote($locale.DIRECTORY_SEPARATOR, '/').'/', '', $file)); 31 | 32 | $translationFile = TranslationFile::firstOrCreate([ 33 | 'name' => $filePath, 34 | 'extension' => $extension, 35 | 'is_root' => $isRoot, 36 | ]); 37 | 38 | $key = config('translations.include_file_in_key') && ! $isRoot ? "{$translationFile->name}.{$key}" : $key; 39 | $method = $overwrite ? 'updateOrCreate' : 'firstOrCreate'; 40 | $translation->phrases()->$method([ 41 | 'key' => $key, 42 | 'group' => $translationFile->name, 43 | 'translation_file_id' => $translationFile->id, 44 | ], [ 45 | 'value' => (empty($value) ? null : $value), 46 | 'parameters' => is_string($value) ? getPhraseParameters($value) : null, 47 | 'phrase_id' => $translation->source ? null : $source->phrases()->where('key', $key)->where('group', $translationFile->name)->first()?->id, 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Console/Commands/CleanOldVersionCommand.php: -------------------------------------------------------------------------------- 1 | where('migration', 'like', '%create_translations_tables%')->delete(); 34 | } 35 | 36 | // remove old config file 37 | File::delete(config_path('translations.php')); 38 | 39 | // remove old service provider 40 | $this->unregisterServiceProvider(); 41 | 42 | $this->info('Old version of Translations UI was cleaned successfully.'); 43 | 44 | return self::SUCCESS; 45 | } 46 | 47 | protected function unregisterServiceProvider(): void 48 | { 49 | $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace()); 50 | 51 | $appConfig = file_get_contents(config_path('app.php')); 52 | 53 | if (Str::contains($appConfig, $namespace.'\\Providers\\TranslationsServiceProvider::class')) { 54 | file_put_contents(config_path('app.php'), str_replace( 55 | "$namespace\\Providers\\TranslationsServiceProvider::class,".PHP_EOL, 56 | '', 57 | $appConfig 58 | )); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Console/Commands/ExportTranslationsCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 21 | } 22 | 23 | public function handle(): void 24 | { 25 | $this->info('Exporting translations...'.PHP_EOL); 26 | 27 | $this->manager->export(); 28 | 29 | $this->info('Translations exported successfully!'.PHP_EOL); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Console/Commands/PublishCommand.php: -------------------------------------------------------------------------------- 1 | option('force'); 17 | 18 | if (! $force && File::exists(public_path('vendor/translations-ui'))) { 19 | $this->line('Your application already have the Translations UI assets'); 20 | 21 | if (! $this->confirm('Do you want to rewrite?')) { 22 | return self::FAILURE; 23 | } 24 | } 25 | 26 | File::deleteDirectory(public_path('vendor/translations-ui')); 27 | File::copyDirectory(__DIR__.'/../../../resources/dist/vendor', public_path('vendor')); 28 | File::copy(__DIR__.'/../../../resources/favicon.ico', public_path('vendor/translations-ui/favicon.ico')); 29 | 30 | $this->info('Assets was published to [public/vendor/translations-ui]'); 31 | 32 | return self::SUCCESS; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Enums/RoleEnum.php: -------------------------------------------------------------------------------- 1 | 'Owner', 14 | self::translator => 'Translator', 15 | }; 16 | } 17 | 18 | public static function fromLabel($label): self 19 | { 20 | return match ($label) { 21 | 'Owner', 'owner' => self::owner, 22 | 'Translator', 'translator' => self::translator, 23 | default => self::owner, 24 | }; 25 | } 26 | 27 | public function description(): string 28 | { 29 | return match ($this) { 30 | self::owner => 'Full access to everything', 31 | self::translator => 'Can translate phrases for a language or multiple languages', 32 | }; 33 | } 34 | 35 | public static function toSelectArray(): array 36 | { 37 | return collect(self::cases())->map(function (RoleEnum $role) { 38 | return [ 39 | 'value' => $role->value, 40 | 'label' => $role->label(), 41 | 'description' => $role->description(), 42 | ]; 43 | })->toArray(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Enums/StatusEnum.php: -------------------------------------------------------------------------------- 1 | 'Active', 16 | self::inactive => 'Inactive', 17 | self::deprecated => 'Deprecated', 18 | self::needs_update => 'Needs Update', 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | check()) { 17 | return redirect()->route('ltu.translation.index'); 18 | } 19 | 20 | return Inertia::render('auth/login'); 21 | } 22 | 23 | public function store(LoginRequest $request): RedirectResponse 24 | { 25 | $request->authenticate(); 26 | 27 | $request->session()->regenerate(); 28 | 29 | return redirect()->route('ltu.translation.index'); 30 | } 31 | 32 | public function destroy(Request $request): RedirectResponse 33 | { 34 | Auth::guard('translations')->logout(); 35 | 36 | $request->session()->invalidate(); 37 | 38 | $request->session()->regenerateToken(); 39 | 40 | return redirect()->route('ltu.login'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/InvitationAcceptController.php: -------------------------------------------------------------------------------- 1 | first()) { 18 | abort(404); 19 | } 20 | 21 | return Inertia::render('auth/invite/accept', [ 22 | 'email' => $invite->email, 23 | 'token' => $invite->token, 24 | ]); 25 | } 26 | 27 | public function store(Request $request): RedirectResponse 28 | { 29 | $request->validate([ 30 | 'name' => 'required|string', 31 | 'token' => 'required|string', 32 | 'password' => 'required|string|confirmed|min:8', 33 | ]); 34 | 35 | if (! $invite = Invite::where('token', $request->input('token'))->first()) { 36 | abort(404); 37 | } 38 | 39 | if (Contributor::where('email', $invite->email)->first()) { 40 | return redirect()->route('ltu.login') 41 | ->with('notification', [ 42 | 'type' => 'error', 43 | 'body' => 'You already have an account, please login', 44 | ]); 45 | } 46 | 47 | $user = Contributor::create([ 48 | 'role' => $invite->role, 49 | 'email' => $invite->email, 50 | 'name' => $request->input('name'), 51 | 'password' => Hash::make($request->input('password')), 52 | ]); 53 | 54 | $invite->delete(); 55 | 56 | auth('translations')->login($user); 57 | 58 | return redirect()->route('ltu.translation.index'); 59 | } 60 | 61 | public function destroy(Invite $invite) 62 | { 63 | $invite->delete(); 64 | 65 | return redirect()->route('ltu.contributors.index')->withFragment('#invited')->with('notification', [ 66 | 'type' => 'success', 67 | 'body' => 'Invitation deleted successfully', 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | first()) { 24 | abort(404); 25 | } 26 | 27 | return Inertia::render('auth/reset-password', [ 28 | 'token' => $token, 29 | 'email' => $user->email, 30 | ]); 31 | } 32 | 33 | public function store(Request $request) 34 | { 35 | $request->validate([ 36 | 'token' => 'required', 37 | 'email' => 'required|email', 38 | 'password' => 'required|confirmed|min:8', 39 | ]); 40 | 41 | try { 42 | [$id, $token] = explode('|', decrypt($request->input('token'))); 43 | 44 | $user = Contributor::findOrFail($id); 45 | 46 | // Here we will attempt to reset the user's password. If it is successful we 47 | // will update the password on an actual user model and persist it to the 48 | // database. Otherwise we will parse the error and return the response. 49 | $user->password = Hash::make($request->input('password')); 50 | 51 | $user->setRememberToken(Str::random(60)); 52 | 53 | $user->save(); 54 | 55 | Auth::guard('translations')->login($user); 56 | } catch (Throwable $e) { 57 | return redirect()->route('ltu.password.request')->with('invalidResetToken', 'Invalid token'); 58 | } 59 | 60 | cache()->forget("password.reset.$id"); 61 | 62 | // If the password was successfully reset, we will redirect the user back to 63 | // the application's home authenticated view. If there is an error we can 64 | // redirect them back to where they came from with their error message. 65 | return redirect()->route('ltu.translation.index'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | validate([ 26 | 'email' => 'required|email|exists:'.($connection ? $connection.'.' : '').'ltu_contributors,email', 27 | ]); 28 | 29 | $token = Str::random(); 30 | 31 | $user = Contributor::firstWhere('email', $request->email); 32 | 33 | if ($user) { 34 | cache( 35 | ["password.reset.$user->id" => $token], 36 | now()->addMinutes(60) 37 | ); 38 | 39 | // We will send the password reset link to this user. Once we have attempted 40 | // to send the link, we will examine the response then see the message we 41 | // need to show to the user. Finally, we'll send out a proper response. 42 | Mail::to($user->email)->send(new ResetPassword(encrypt("{$user->id}|{$token}"))); 43 | } 44 | 45 | return redirect() 46 | ->route('ltu.password.request') 47 | ->with('status', 'We have emailed your password reset link!'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Http/Controllers/ContributorController.php: -------------------------------------------------------------------------------- 1 | InviteResource::collection(Invite::paginate(10)), 26 | 'contributors' => ContributorResource::collection(Contributor::paginate(10)), 27 | ]); 28 | } 29 | 30 | public function create(): Modal 31 | { 32 | return Inertia::modal('contributor/modals/invite', [ 33 | 'roles' => RoleEnum::toSelectArray(), 34 | ])->baseRoute('ltu.contributors.index'); 35 | } 36 | 37 | public function store(Request $request): RedirectResponse 38 | { 39 | $connection = config('translations.database_connection'); 40 | $request->validate([ 41 | 'role' => 'required|integer', 42 | 'email' => 'required|email|unique:'.($connection ? $connection.'.' : '').'ltu_contributors,email', 43 | ]); 44 | 45 | do { 46 | $token = Str::random(32); 47 | } while (Invite::where('token', $token)->first()); 48 | 49 | $invite = Invite::create([ 50 | 'token' => $token, 51 | 'role' => $request->get('role'), 52 | 'email' => $request->get('email'), 53 | ]); 54 | 55 | Mail::to($request->get('email'))->send(new InviteCreated($invite)); 56 | 57 | return redirect()->route('ltu.contributors.index')->withFragment('#invited')->with('notification', [ 58 | 'type' => 'success', 59 | 'body' => 'Invite sent successfully', 60 | ]); 61 | } 62 | 63 | public function destroy(Contributor $contributor): RedirectResponse 64 | { 65 | $contributor->delete(); 66 | 67 | return redirect()->route('ltu.contributors.index')->with('notification', [ 68 | 'type' => 'success', 69 | 'body' => 'Contributor deleted successfully', 70 | ]); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Http/Controllers/ProfileController.php: -------------------------------------------------------------------------------- 1 | session('status'), 19 | ]); 20 | } 21 | 22 | public function update(Request $request): RedirectResponse 23 | { 24 | $connection = config('translations.database_connection'); 25 | $request->validate([ 26 | 'name' => ['required', 'max:255'], 27 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.($connection ? $connection.'.' : '').'ltu_contributors,email,'.$request->user()->id], 28 | ]); 29 | 30 | $request->user()->update($request->only('name', 'email')); 31 | 32 | return redirect()->route('ltu.profile.edit'); 33 | } 34 | 35 | public function updatePassword(Request $request): RedirectResponse 36 | { 37 | $validated = $request->validate([ 38 | 'current_password' => ['required', 'current_password'], 39 | 'password' => ['required', Password::defaults(), 'confirmed'], 40 | ]); 41 | 42 | $request->user()->update([ 43 | 'password' => Hash::make($validated['password']), 44 | ]); 45 | 46 | return redirect()->route('ltu.profile.edit') 47 | ->with('status', 'password-updated'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 17 | } 18 | 19 | /** 20 | * @throws AuthenticationException 21 | */ 22 | public function handle(Request $request, Closure $next) 23 | { 24 | if ($this->auth->guard('translations')->check()) { 25 | $this->auth->shouldUse('translations'); 26 | } else { 27 | throw new AuthenticationException( 28 | 'Unauthenticated.', ['translations'], route('ltu.login') 29 | ); 30 | } 31 | 32 | return $next($request); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | $this->auth(), 29 | 'ziggy' => function () use ($request) { 30 | return array_merge((new Ziggy)->toArray(), [ 31 | 'location' => $request->url(), 32 | ]); 33 | }, 34 | 'notification' => fn () => $request->session()->get('notification'), 35 | 'status' => fn () => $request->session()->get('status'), 36 | ]); 37 | } 38 | 39 | protected function auth(): array 40 | { 41 | if (! Auth::guard('translations')->check()) { 42 | return [ 43 | 'user' => null, 44 | ]; 45 | } 46 | 47 | $user = Auth::guard('translations')->user(); 48 | 49 | if (! $user instanceof Contributor) { 50 | return []; 51 | } 52 | 53 | return [ 54 | 'user' => new ContributorResource($user), 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Http/Middleware/RedirectIfNotOwner.php: -------------------------------------------------------------------------------- 1 | isOwner()) { 13 | return redirect()->back()->with('notification', [ 14 | 'type' => 'error', 15 | 'body' => 'You are not allowed to perform this action', 16 | ]); 17 | } 18 | 19 | return $next($request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Requests/LoginRequest.php: -------------------------------------------------------------------------------- 1 | redirect = route('ltu.login'); 19 | 20 | return [ 21 | 'email' => 'required|string|email', 22 | 'password' => 'required|string', 23 | ]; 24 | } 25 | 26 | public function authenticate(): void 27 | { 28 | if (! Auth::guard('translations')->attempt($this->only('email', 'password'), $this->filled('remember'))) { 29 | throw ValidationException::withMessages([ 30 | 'email' => __('auth.failed'), 31 | ]); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Resources/ContributorResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 18 | 'name' => $this->name, 19 | 'role' => [ 20 | 'value' => $this->role->value, 21 | 'label' => $this->role->label(), 22 | ], 23 | 'email' => $this->email, 24 | 'avatar' => $this->avatar, 25 | 'created_at' => $this->created_at, 26 | 'updated_at' => $this->updated_at, 27 | ]; 28 | } 29 | 30 | /** 31 | * Create an AnonymousResourceCollection without wrapping 32 | * 33 | * @see JsonResource::newCollection() 34 | * 35 | * @param mixed|Collection $resource 36 | * @return UnwrappedAnonymousResourceCollection 37 | */ 38 | protected static function newCollection($resource) 39 | { 40 | return new UnwrappedAnonymousResourceCollection($resource, ContributorResource::class); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Resources/InviteResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'email' => $this->email, 20 | 'token' => $this->token, 21 | 'role' => [ 22 | 'value' => $this->role->value, 23 | 'label' => $this->role->label(), 24 | ], 25 | 'languages' => $this->languages, 26 | 'invited_at' => $this->created_at->format('D, d M Y h:i A'), 27 | ]; 28 | } 29 | 30 | /** 31 | * Create an AnonymousResourceCollection without wrapping 32 | * 33 | * @see JsonResource::newCollection() 34 | * 35 | * @param mixed|Collection $resource 36 | * @return UnwrappedAnonymousResourceCollection 37 | */ 38 | protected static function newCollection($resource) 39 | { 40 | return new UnwrappedAnonymousResourceCollection($resource, InviteResource::class); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Resources/LanguageResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 18 | 'name' => $this->name, 19 | 'code' => $this->code, 20 | 'rtl' => $this->rtl, 21 | ]; 22 | } 23 | 24 | /** 25 | * Create an AnonymousResourceCollection without wrapping 26 | * 27 | * @see JsonResource::newCollection() 28 | * 29 | * @param mixed|Collection $resource 30 | * @return UnwrappedAnonymousResourceCollection 31 | */ 32 | protected static function newCollection($resource) 33 | { 34 | return new UnwrappedAnonymousResourceCollection($resource, LanguageResource::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Http/Resources/PhraseResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'uuid' => $this->uuid, 20 | 'key' => $this->key, 21 | 'group' => $this->group, 22 | 'value' => $this->value, 23 | 'parameters' => $this->parameters, 24 | 'created_at' => $this->created_at, 25 | 'updated_at' => $this->updated_at, 26 | 'state' => (bool) $this->value, 27 | 'note' => $this->note, 28 | 'value_html' => $this->splitParameters(), 29 | 'translation_id' => $this->translation_id, 30 | 'translation_file_id' => $this->translation_file_id, 31 | 'phrase_id' => $this->phrase_id, 32 | 'file' => TranslationFileResource::make($this->whenLoaded('file')), 33 | 'translation' => TranslationResource::make($this->whenLoaded('translation')), 34 | 'source' => PhraseResource::make($this->whenLoaded('source')), 35 | ]; 36 | } 37 | 38 | private function splitParameters(): array 39 | { 40 | if (blank($this->parameters)) { 41 | return []; 42 | } 43 | 44 | $result = collect(); 45 | 46 | foreach (explode(' ', $this->value) as $word) { 47 | if (preg_match('/(?push([ 49 | 'parameter' => true, 50 | 'value' => $word, 51 | ]); 52 | } else { 53 | $result->push([ 54 | 'parameter' => false, 55 | 'value' => $word, 56 | ]); 57 | } 58 | } 59 | 60 | return $result->toArray(); 61 | } 62 | 63 | /** 64 | * Create an AnonymousResourceCollection without wrapping 65 | * 66 | * @see JsonResource::newCollection() 67 | * 68 | * @param mixed|Collection $resource 69 | * @return UnwrappedAnonymousResourceCollection 70 | */ 71 | protected static function newCollection($resource) 72 | { 73 | return new UnwrappedAnonymousResourceCollection($resource, PhraseResource::class); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Http/Resources/TranslationFileResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 20 | 'name' => $this->name, 21 | 'extension' => $this->extension, 22 | 'nameWithExtension' => "$this->name.$this->extension", 23 | 'phrases' => PhraseResource::collection($this->whenLoaded('phrases')), 24 | ]; 25 | } 26 | 27 | /** 28 | * Create an AnonymousResourceCollection without wrapping 29 | * 30 | * @see JsonResource::newCollection() 31 | * 32 | * @param mixed|Collection $resource 33 | * @return UnwrappedAnonymousResourceCollection 34 | * @return AnonymousResourceCollection 35 | */ 36 | protected static function newCollection($resource) 37 | { 38 | return new UnwrappedAnonymousResourceCollection($resource, TranslationFileResource::class); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Http/Resources/TranslationResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 22 | 'language' => LanguageResource::make($this->whenLoaded('language')), 23 | 'source' => $this->source, 24 | 'created_at' => $this->created_at->format('Y-m-d H:i:s'), 25 | 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), 26 | 'progress' => $this->formatProgress(), 27 | 'phrases_count' => $this->phrases_count, 28 | ]; 29 | } 30 | 31 | private function formatProgress(): string 32 | { 33 | if ($this->progress > 0) { 34 | return "{$this->progress}%"; 35 | } 36 | 37 | return '0%'; 38 | } 39 | 40 | /** 41 | * Create an AnonymousResourceCollection without wrapping 42 | * 43 | * @see JsonResource::newCollection() 44 | * 45 | * @param mixed|Collection $resource 46 | * @return UnwrappedAnonymousResourceCollection 47 | */ 48 | protected static function newCollection($resource) 49 | { 50 | return new UnwrappedAnonymousResourceCollection($resource, TranslationResource::class); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Http/Resources/UnwrappedAnonymousResourceCollection.php: -------------------------------------------------------------------------------- 1 | subject('You have been invited to join the translation team') 19 | ->markdown('translations::mail.invite', [ 20 | 'link' => route('ltu.invitation.accept', $this->invite->token), 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Mail/ResetPassword.php: -------------------------------------------------------------------------------- 1 | token = $token; 18 | } 19 | 20 | public function build(): static 21 | { 22 | return $this->subject('Reset your password') 23 | ->markdown('translations::mail.password', [ 24 | 'link' => route('ltu.password.reset', ['token' => $this->token]), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Models/Contributor.php: -------------------------------------------------------------------------------- 1 | RoleEnum::class, 28 | ]; 29 | 30 | public function isOwner(): bool 31 | { 32 | return $this->role === RoleEnum::owner; 33 | } 34 | 35 | public function isTranslator(): bool 36 | { 37 | return $this->role === RoleEnum::translator; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Models/Invite.php: -------------------------------------------------------------------------------- 1 | RoleEnum::class, 28 | 'languages' => 'json', 29 | ]; 30 | } 31 | -------------------------------------------------------------------------------- /src/Models/Language.php: -------------------------------------------------------------------------------- 1 | hasOne(Translation::class); 25 | } 26 | 27 | public function phrases(): HasManyThrough 28 | { 29 | return $this->hasManyThrough(Phrase::class, Translation::class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Models/Phrase.php: -------------------------------------------------------------------------------- 1 | 'array', 24 | ]; 25 | 26 | protected $with = [ 27 | 'source', 'file', 28 | ]; 29 | 30 | public function file(): BelongsTo 31 | { 32 | return $this->belongsTo(TranslationFile::class, 'translation_file_id'); 33 | } 34 | 35 | public function source(): BelongsTo 36 | { 37 | return $this->belongsTo(Phrase::class, 'phrase_id'); 38 | } 39 | 40 | public function translation(): BelongsTo 41 | { 42 | return $this->belongsTo(Translation::class); 43 | } 44 | 45 | public function similarPhrases(): Collection 46 | { 47 | return $this->translation->phrases()->where('key', 'like', "%$this->key%") 48 | ->whereKeyNot($this->id) 49 | ->get(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Models/Translation.php: -------------------------------------------------------------------------------- 1 | 'boolean', 22 | ]; 23 | 24 | protected $with = [ 25 | 'language', 26 | ]; 27 | 28 | public function phrases(): HasMany 29 | { 30 | return $this->hasMany(Phrase::class); 31 | } 32 | 33 | public function language(): BelongsTo 34 | { 35 | return $this->belongsTo(Language::class); 36 | } 37 | 38 | public function scopeIsSource($query): void 39 | { 40 | $query->where('source', true); 41 | } 42 | 43 | public function scopeWithProgress($query): void 44 | { 45 | $query->addSelect([ 46 | 'progress' => Phrase::selectRaw('AVG(CASE WHEN value IS NOT NULL THEN 1 ELSE 0 END) * 100') 47 | ->whereColumn('ltu_phrases.translation_id', 'ltu_translations.id') 48 | ->limit(1), 49 | ])->withCasts([ 50 | 'progress' => 'decimal:1', 51 | ]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Models/TranslationFile.php: -------------------------------------------------------------------------------- 1 | 'boolean', 24 | ]; 25 | 26 | public function phrases(): HasMany 27 | { 28 | return $this->hasMany(Phrase::class); 29 | } 30 | 31 | public function fileName(): Attribute 32 | { 33 | return Attribute::get(function () { 34 | return "$this->name.$this->extension"; 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Traits/HasDatabaseConnection.php: -------------------------------------------------------------------------------- 1 | uuid = Str::orderedUuid()->toString(); 14 | }); 15 | } 16 | 17 | public static function findByUuid(string $uuid): ?Model 18 | { 19 | return static::where('uuid', $uuid)->firstOrFail(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import forms from "@tailwindcss/forms" 2 | import defaultTheme from "tailwindcss/defaultTheme" 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: ["./node_modules/@protonemedia/inertiajs-tables-laravel-query-builder/**/*.{js,vue}", "./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php", "./storage/framework/views/*.php", "./resources/views/**/*.blade.php", "./resources/views/**/*.vue", "./resources/views/**/*.js"], 7 | 8 | theme: { 9 | extend: { 10 | fontFamily: { 11 | sans: ["Inter var", ...defaultTheme.fontFamily.sans], 12 | }, 13 | }, 14 | }, 15 | 16 | plugins: [forms], 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "jsx": "preserve", 7 | "strict": true, 8 | "isolatedModules": true, 9 | "target": "ESNext", 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "skipLibCheck": true, 14 | "paths": { 15 | "@/*": ["/resources/*"], 16 | "ziggy-js": ["./vendor/tightenco/ziggy"] 17 | }, 18 | "types": ["vite/client"], 19 | }, 20 | "include": ["resources/**/*.ts", "resources/**/*.d.ts", "resources/**/*.vue"] 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import laravel from "laravel-vite-plugin" 3 | import vue from "@vitejs/plugin-vue" 4 | import autoimport from "unplugin-auto-import/vite" 5 | import components from "unplugin-vue-components/vite" 6 | 7 | export default defineConfig({ 8 | resolve: { 9 | alias: { 10 | "@": "/resources", 11 | "~": "/node_modules", 12 | }, 13 | }, 14 | plugins: [ 15 | laravel({ 16 | input: "resources/scripts/app.ts", 17 | ssr: "resources/scripts/ssr.ts", 18 | publicDirectory: "resources/dist", 19 | buildDirectory: "vendor/translations-ui", 20 | refresh: true, 21 | }), 22 | vue({ 23 | template: { 24 | transformAssetUrls: { 25 | base: null, 26 | includeAbsolute: false, 27 | }, 28 | }, 29 | script: { 30 | defineModel: true, 31 | propsDestructure: true, 32 | }, 33 | }), 34 | autoimport({ 35 | vueTemplate: true, 36 | dts: "resources/scripts/types/auto-imports.d.ts", 37 | dirs: ["resources/scripts/composables", "resources/scripts/utils"], 38 | imports: [ 39 | "vue", 40 | "@vueuse/core", 41 | { 42 | "momentum-lock": ["can"], 43 | }, 44 | { 45 | "momentum-modal": ["useModal"], 46 | }, 47 | { 48 | "@inertiajs/vue3": ["router", "useForm", "usePage", "useRemember"], 49 | }, 50 | ], 51 | }), 52 | components({ 53 | dirs: ["resources/views/components"], 54 | dts: "resources/scripts/types/components.d.ts", 55 | resolvers: [ 56 | (name) => { 57 | const components = ["Link", "Head"] 58 | 59 | if (components.includes(name)) { 60 | return { 61 | name: name, 62 | from: "@inertiajs/vue3", 63 | } 64 | } 65 | }, 66 | 67 | (name) => { 68 | if (name.startsWith("Layout")) { 69 | const componentName = name.substring("Layout".length).toLowerCase() 70 | 71 | return { 72 | name: "default", 73 | from: `@/views/layouts/${componentName}/layout-${componentName}.vue`, 74 | } 75 | } 76 | }, 77 | ], 78 | }), 79 | ], 80 | build: { 81 | manifest: true, 82 | // ... other build options 83 | } 84 | }) 85 | 86 | --------------------------------------------------------------------------------