diff --git a/.env.example b/.env.example index 000109b..ab28683 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,9 @@ APP_KEY= APP_DEBUG=true APP_URL=http://localhost -APP_LOCALE=en -APP_FALLBACK_LOCALE=en -APP_FAKER_LOCALE=en_US +APP_LOCALE=es +APP_FALLBACK_LOCALE=es +APP_FAKER_LOCALE=es_MX APP_MAINTENANCE_DRIVER=file # APP_MAINTENANCE_STORE=database diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 2547d72..0dc363d 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,41 +1,83 @@ #!/usr/bin/env bash # Tekitl pre-commit hook -# Blocks commits whose staged JS/TS changes fail ESLint. Honors Constitution -# (Principle VI Code Hygiene + Development Workflow lint gate). +# Blocks commits whose staged JS/TS changes fail ESLint and whose staged +# user-facing files contain hard-coded literals (i18n audit). Honors +# Constitution (Principle VI Code Hygiene + Development Workflow lint gate). # # Activated automatically by the `prepare` npm script which sets # `git config core.hooksPath .githooks` after dependency install. set -euo pipefail +# ---- Stage 1: ESLint on staged JS/TS ------------------------------------ + # Collect staged JS/TS files (Added, Copied, Modified, Renamed). mapfile -t staged < <(git diff --cached --name-only --diff-filter=ACMR \ -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.cjs' '*.mjs') -if [ "${#staged[@]}" -eq 0 ]; then - exit 0 -fi +if [ "${#staged[@]}" -gt 0 ]; then + # Reject partially staged JS/TS files: ESLint reads the working tree, so + # linting partially staged content would produce a false pass or fail and + # undermine the staged-content gate. Force the developer to stage the full + # file (or stash unstaged hunks) so the lint result matches what is committed. + if ! git diff --quiet -- "${staged[@]}"; then + echo "[pre-commit] ERROR: partially staged JS/TS files detected." >&2 + echo "[pre-commit] Stage full files (or stash unstaged hunks) before commit." >&2 + exit 1 + fi -# Reject partially staged JS/TS files: ESLint reads the working tree, so -# linting partially staged content would produce a false pass or fail and -# undermine the staged-content gate. Force the developer to stage the full -# file (or stash unstaged hunks) so the lint result matches what is committed. -if ! git diff --quiet -- "${staged[@]}"; then - echo "[pre-commit] ERROR: partially staged JS/TS files detected." >&2 - echo "[pre-commit] Stage full files (or stash unstaged hunks) before commit." >&2 - exit 1 + echo "[pre-commit] Running ESLint on ${#staged[@]} staged file(s)..." + + # `--` terminates option parsing so a filename starting with `-` cannot be + # misread as an ESLint flag. + if command -v bun >/dev/null 2>&1; then + bun x eslint --no-warn-ignored -- "${staged[@]}" + elif command -v npx >/dev/null 2>&1; then + npx --no-install eslint --no-warn-ignored -- "${staged[@]}" + else + echo "[pre-commit] ERROR: neither 'bun' nor 'npx' found in PATH." >&2 + echo "[pre-commit] Install Bun or Node.js and run 'bun install' (or 'npm install')." >&2 + exit 1 + fi fi -echo "[pre-commit] Running ESLint on ${#staged[@]} staged file(s)..." - -# `--` terminates option parsing so a filename starting with `-` cannot be -# misread as an ESLint flag. -if command -v bun >/dev/null 2>&1; then - bun x eslint --no-warn-ignored -- "${staged[@]}" -elif command -v npx >/dev/null 2>&1; then - npx --no-install eslint --no-warn-ignored -- "${staged[@]}" -else - echo "[pre-commit] ERROR: neither 'bun' nor 'npx' found in PATH." >&2 - echo "[pre-commit] Install Bun or Node.js and run 'bun install' (or 'npm install')." >&2 - exit 1 +# ---- Stage 2: i18n audit on staged user-facing files -------------------- +# Contract: specs/002-i18n-spanish-baseline/contracts/audit-cli.md +# § "Pre-commit Hook Integration" + +mapfile -t i18n_staged < <(git diff --cached --name-only --diff-filter=ACMR \ + -- '*.php' '*.blade.php' '*.tsx' '*.ts' '*.jsx' '*.js') + +# Drop paths the audit config excludes (tests, fixtures, vendored bundles, the +# i18n helper itself). Keeping these in sync with `tools/i18n/audit.config.json` +# is intentional: the hook is a thin staged-subset gate over the same audit +# binary, and explicit positional paths bypass the binary's glob-level exclude. +i18n_filtered=() +for staged_path in "${i18n_staged[@]}"; do + case "$staged_path" in + tests/*|tools/i18n/fixtures/*|node_modules/*|vendor/*|storage/*|bootstrap/cache/*|public/build/*) ;; + resources/js/components/examples/*|resources/js/wayfinder/*|resources/js/types/*) ;; + resources/js/actions/*|resources/js/routes/*|resources/js/lib/i18n.ts) ;; + *) i18n_filtered+=("$staged_path") ;; + esac +done + +if [ "${#i18n_filtered[@]}" -gt 0 ]; then + if ! git diff --quiet -- "${i18n_filtered[@]}"; then + echo "[pre-commit] ERROR: partially staged user-facing files detected." >&2 + echo "[pre-commit] Stage full files (or stash unstaged hunks) before commit." >&2 + exit 1 + fi + + echo "[pre-commit] Running i18n audit on ${#i18n_filtered[@]} staged file(s)..." + + if command -v bun >/dev/null 2>&1; then + bun run i18n:audit -- "${i18n_filtered[@]}" + elif command -v node >/dev/null 2>&1; then + node tools/i18n/audit.mjs -- "${i18n_filtered[@]}" + else + echo "[pre-commit] ERROR: neither 'bun' nor 'node' found in PATH." >&2 + echo "[pre-commit] Install Bun or Node.js to run the i18n audit." >&2 + exit 1 + fi fi diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 619061f..106eb25 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -42,6 +42,9 @@ jobs: - name: Lint Frontend run: npm run lint + - name: i18n Audit + run: npm run i18n:audit + # - name: Commit Changes # uses: stefanzweifel/git-auto-commit-action@v7 # with: diff --git a/.specify/feature.json b/.specify/feature.json index 477be2f..76d2093 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1 +1 @@ -{"feature_directory":"specs/001-project-lifecycle-timeline"} +{"feature_directory":"specs/002-i18n-spanish-baseline"} diff --git a/CLAUDE.md b/CLAUDE.md index b317345..a85c179 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,7 @@ This project has domain-specific skills available. You MUST activate the relevan - You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. - Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. - Check for existing components to reuse before writing a new one. +- User-facing copy lives only in `lang//`. Use `__()` / `@lang` (PHP/Blade) or `t()` / `tChoice()` from `@/lib/i18n` (React). Procedure: [`specs/002-i18n-spanish-baseline/quickstart.md`](specs/002-i18n-spanish-baseline/quickstart.md). ## Verification Scripts @@ -293,6 +294,6 @@ Wayfinder generates TypeScript functions for Laravel routes. Import from `@/acti -Active feature plan: [specs/001-project-lifecycle-timeline/plan.md](specs/001-project-lifecycle-timeline/plan.md) +Active feature plan: [specs/002-i18n-spanish-baseline/plan.md](specs/002-i18n-spanish-baseline/plan.md) Read the plan for technical context, project structure, and constitutional compliance for the in-flight feature. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3e5714c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing to Tekitl + +## Adding a new string + +User-facing copy (Spanish or any other locale) MUST live in `lang//`. Hard-coded literals in PHP, Blade, or React are blocked by the i18n audit (`bun run i18n:audit`) and rejected at the pre-commit hook plus CI. + +1. Add the key to `lang/es/.php` (e.g., `lang/es/projects.php`). Keys are dotted paths in `snake_case`. +2. Run `php artisan i18n:scaffold-en` to mirror the key into `lang/en/.php` with an empty value. +3. Reference the key from code: + - **PHP / Blade**: `__('domain.section.key')` or `@lang('domain.section.key')`. + - **React / TS**: `import { t, tChoice } from '@/lib/i18n'` then `t('domain.section.key')`. +4. Run `bun run i18n:audit`; expect `0 finding(s).` +5. Commit. The pre-commit hook re-runs ESLint and the audit on the staged subset. + +Full procedure, plural rules, allow-list policy, and false-positive handling: [`specs/002-i18n-spanish-baseline/quickstart.md`](specs/002-i18n-spanish-baseline/quickstart.md). + +The translation helper contract (signatures, failure modes, type surface): [`specs/002-i18n-spanish-baseline/contracts/translation-helper.md`](specs/002-i18n-spanish-baseline/contracts/translation-helper.md). diff --git a/app/ConfidenceLevel.php b/app/ConfidenceLevel.php index 7ea2a5d..922bd17 100644 --- a/app/ConfidenceLevel.php +++ b/app/ConfidenceLevel.php @@ -4,7 +4,7 @@ enum ConfidenceLevel: string { - case Aprendiz = 'aprendiz'; - case Autosuficiente = 'autosuficiente'; - case Maestro = 'maestro'; + case Apprentice = 'aprendiz'; + case SelfSufficient = 'autosuficiente'; + case Master = 'maestro'; } diff --git a/app/Console/Commands/I18nReport.php b/app/Console/Commands/I18nReport.php new file mode 100644 index 0000000..ce1e371 --- /dev/null +++ b/app/Console/Commands/I18nReport.php @@ -0,0 +1,152 @@ +/.'; + + public function handle(): int + { + $base = $this->laravel->langPath(); + $defaultLocale = (string) config('app.locale', 'es'); + + $locales = $this->resolveLocales($base, $defaultLocale); + $format = (string) $this->option('format'); + + if (! in_array($format, ['text', 'json'], true)) { + $this->components->error("Unknown --format value: {$format}"); + + return self::FAILURE; + } + + $reports = []; + $totalUntranslated = 0; + + foreach ($locales as $locale) { + $untranslated = $this->collectUntranslated($base, $locale); + sort($untranslated); + + $reports[] = [ + 'locale' => $locale, + 'untranslated' => $untranslated, + 'total' => count($untranslated), + ]; + + $totalUntranslated += count($untranslated); + } + + if ($format === 'json') { + $payload = count($reports) === 1 ? $reports[0] : ['locales' => $reports, 'total' => $totalUntranslated]; + $this->line((string) json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } else { + foreach ($reports as $report) { + $this->line(sprintf('[%s] %d untranslated key(s):', $report['locale'], $report['total'])); + foreach ($report['untranslated'] as $key) { + $this->line(' - '.$key); + } + } + } + + if ((bool) $this->option('strict') && $totalUntranslated > 0) { + return self::FAILURE; + } + + return self::SUCCESS; + } + + /** + * @return array + */ + private function resolveLocales(string $base, string $defaultLocale): array + { + $explicit = $this->option('locale'); + if (is_string($explicit) && $explicit !== '') { + return [$explicit]; + } + + $found = []; + foreach (glob($base.DIRECTORY_SEPARATOR.'*', GLOB_ONLYDIR) ?: [] as $dir) { + $name = basename($dir); + if ($name !== $defaultLocale) { + $found[] = $name; + } + } + + sort($found); + + return $found; + } + + /** + * @return array + */ + private function collectUntranslated(string $base, string $locale): array + { + $localeDir = $base.DIRECTORY_SEPARATOR.$locale; + + if (! is_dir($localeDir)) { + return []; + } + + $sourceDir = $base.DIRECTORY_SEPARATOR.((string) config('app.locale', 'es')); + + $untranslated = []; + + foreach (glob($localeDir.DIRECTORY_SEPARATOR.'*.php') ?: [] as $path) { + $domain = basename($path, '.php'); + $tree = require $path; + + if (! is_array($tree)) { + continue; + } + + $sourceTree = is_file($sourceDir.DIRECTORY_SEPARATOR.$domain.'.php') + ? (array) require $sourceDir.DIRECTORY_SEPARATOR.$domain.'.php' + : []; + + $this->walk($tree, $sourceTree, $domain, $untranslated); + } + + return $untranslated; + } + + /** + * @param array $tree + * @param array $sourceTree + * @param array $untranslated + */ + private function walk(array $tree, array $sourceTree, string $prefix, array &$untranslated): void + { + foreach ($tree as $key => $value) { + $path = $prefix.'.'.$key; + $sourceValue = is_array($sourceTree) ? ($sourceTree[$key] ?? null) : null; + + if (is_array($value)) { + $childSource = is_array($sourceValue) ? $sourceValue : []; + $this->walk($value, $childSource, $path, $untranslated); + + continue; + } + + if (! is_string($value) || $value === '') { + $untranslated[] = $path; + + continue; + } + + if (is_string($sourceValue) && $sourceValue !== '' && $value === $sourceValue) { + $untranslated[] = $path; + } + } + } +} diff --git a/app/Console/Commands/I18nScaffoldEn.php b/app/Console/Commands/I18nScaffoldEn.php new file mode 100644 index 0000000..ce598133 --- /dev/null +++ b/app/Console/Commands/I18nScaffoldEn.php @@ -0,0 +1,136 @@ +laravel->langPath(); + $sourceDir = $base.DIRECTORY_SEPARATOR.'es'; + $targetDir = $base.DIRECTORY_SEPARATOR.'en'; + + if (! is_dir($sourceDir)) { + $this->components->error("Spanish source directory missing: {$sourceDir}"); + + return self::FAILURE; + } + + if (! is_dir($targetDir) && ! mkdir($targetDir, 0o755, true) && ! is_dir($targetDir)) { + $this->components->error("Unable to create target directory: {$targetDir}"); + + return self::FAILURE; + } + + $copy = (bool) $this->option('copy'); + $force = (bool) $this->option('force'); + $dryRun = (bool) $this->option('dry-run'); + + $files = glob($sourceDir.DIRECTORY_SEPARATOR.'*.php') ?: []; + sort($files); + + foreach ($files as $sourcePath) { + $domain = basename($sourcePath, '.php'); + $targetPath = $targetDir.DIRECTORY_SEPARATOR.$domain.'.php'; + + $sourceTree = $this->loadArray($sourcePath); + $existingTree = is_file($targetPath) ? $this->loadArray($targetPath) : []; + + $merged = $this->mirror($sourceTree, $existingTree, $copy, $force); + $rendered = $this->render($merged); + + if ($dryRun) { + $this->components->info("[dry-run] {$targetPath}"); + + continue; + } + + file_put_contents($targetPath, $rendered); + $this->components->info("Wrote {$targetPath}"); + } + + return self::SUCCESS; + } + + /** + * @return array + */ + private function loadArray(string $path): array + { + $contents = require $path; + + return is_array($contents) ? $contents : []; + } + + /** + * @param array $source + * @param array $existing + * @return array + */ + private function mirror(array $source, array $existing, bool $copy, bool $force): array + { + $result = []; + + foreach ($source as $key => $value) { + if (is_array($value)) { + $childExisting = is_array($existing[$key] ?? null) ? $existing[$key] : []; + $result[$key] = $this->mirror($value, $childExisting, $copy, $force); + + continue; + } + + $existingValue = $existing[$key] ?? null; + + if (is_string($existingValue) && $existingValue !== '' && ! $force) { + $result[$key] = $existingValue; + + continue; + } + + $result[$key] = $copy && is_string($value) ? $value : ''; + } + + return $result; + } + + /** + * @param array $tree + */ + private function render(array $tree): string + { + return "exportNode($tree, 0).";\n"; + } + + private function exportNode(mixed $value, int $indent): string + { + if (is_array($value)) { + if ($value === []) { + return '[]'; + } + + $pad = str_repeat(' ', $indent); + $padInner = str_repeat(' ', $indent + 1); + + $lines = []; + foreach ($value as $key => $child) { + $keyExport = is_int($key) ? (string) $key : var_export((string) $key, true); + $lines[] = "{$padInner}{$keyExport} => ".$this->exportNode($child, $indent + 1).','; + } + + return "[\n".implode("\n", $lines)."\n{$pad}]"; + } + + return var_export($value, true); + } +} diff --git a/app/Http/Controllers/PostController.php b/app/Http/Controllers/PostController.php index efb6833..e245a65 100644 --- a/app/Http/Controllers/PostController.php +++ b/app/Http/Controllers/PostController.php @@ -23,7 +23,7 @@ public function index(): Response ]) ->when($userId, fn ($query) => $query ->withExists(['reactions as is_liked' => fn ($q) => $q->where('user_id', $userId)->where('type', ReactionType::Like)]) - ->withExists(['reactions as is_powered_by_current_user' => fn ($q) => $q->where('user_id', $userId)->where('type', ReactionType::Potenciar)]) + ->withExists(['reactions as is_endorsed_by_current_user' => fn ($q) => $q->where('user_id', $userId)->where('type', ReactionType::Endorse)]) ) ->latest() ->cursorPaginate(15); @@ -37,7 +37,7 @@ public function index(): Response 'coins' => $post->coins, 'likes' => $post->likes_count, 'isLiked' => (bool) $post->is_liked, - 'isPoweredByCurrentUser' => (bool) $post->is_powered_by_current_user, + 'isEndorsedByCurrentUser' => (bool) $post->is_endorsed_by_current_user, 'comments' => $post->comments_count, 'hasProject' => $post->project !== null, 'isOwner' => $userId && $userId === $post->user_id, @@ -55,13 +55,13 @@ public function store(StorePostRequest $request): RedirectResponse return back(); } - public function potenciar(Post $post): RedirectResponse + public function endorse(Post $post): RedirectResponse { $post->increment('coins', 10); $post->reactions()->firstOrCreate([ 'user_id' => Auth::id(), - 'type' => ReactionType::Potenciar, + 'type' => ReactionType::Endorse, ]); return back(); diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index f0b6b11..6bed891 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -54,7 +54,7 @@ public function show(Post $post): Response if ($userId) { $post->loadExists([ - 'reactions as is_powered_by_current_user' => fn ($q) => $q->where('user_id', $userId)->where('type', ReactionType::Potenciar), + 'reactions as is_endorsed_by_current_user' => fn ($q) => $q->where('user_id', $userId)->where('type', ReactionType::Endorse), ]); } @@ -130,7 +130,7 @@ public function show(Post $post): Response 'date' => $post->created_at->diffForHumans(), 'dateTime' => $post->created_at->toIso8601String(), 'coins' => $post->coins, - 'isPoweredByCurrentUser' => (bool) ($post->is_powered_by_current_user ?? false), + 'isEndorsedByCurrentUser' => (bool) ($post->is_endorsed_by_current_user ?? false), ], 'isOwner' => $isOwner, 'currentUserApplication' => $currentUserApplication ? [ diff --git a/app/Http/Controllers/ProjectVolunteerController.php b/app/Http/Controllers/ProjectVolunteerController.php index e342f14..de408d7 100644 --- a/app/Http/Controllers/ProjectVolunteerController.php +++ b/app/Http/Controllers/ProjectVolunteerController.php @@ -18,7 +18,7 @@ public function store(StoreProjectVolunteerRequest $request, Project $project, P abort_if( ! in_array($project->stage, [ProjectStage::Planning, ProjectStage::InExecution], true), 403, - 'No se aceptan postulaciones en este momento.', + __('projects.volunteers.applications_closed'), ); $role->volunteers()->create([ diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index e38f55d..3d0f1ab 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -29,7 +29,7 @@ public function show(User $user): Response ]) ->when($userId, fn ($query) => $query ->withExists(['reactions as is_liked' => fn ($q) => $q->where('user_id', $userId)->where('type', ReactionType::Like)]) - ->withExists(['reactions as is_powered_by_current_user' => fn ($q) => $q->where('user_id', $userId)->where('type', ReactionType::Potenciar)]) + ->withExists(['reactions as is_endorsed_by_current_user' => fn ($q) => $q->where('user_id', $userId)->where('type', ReactionType::Endorse)]) ) ->latest() ->cursorPaginate(15); @@ -43,7 +43,7 @@ public function show(User $user): Response 'coins' => $post->coins, 'likes' => $post->likes_count, 'isLiked' => (bool) $post->is_liked, - 'isPoweredByCurrentUser' => (bool) $post->is_powered_by_current_user, + 'isEndorsedByCurrentUser' => (bool) $post->is_endorsed_by_current_user, 'comments' => $post->comments_count, 'hasProject' => $post->project !== null, 'isOwner' => $userId && $userId === $post->user_id, diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 6596cf1..da1df4a 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -2,11 +2,17 @@ namespace App\Http\Middleware; +use App\Support\I18n\TranslationLoader; use Illuminate\Http\Request; use Inertia\Middleware; class HandleInertiaRequests extends Middleware { + public function __construct(protected TranslationLoader $translationLoader) + { + // + } + /** * The root template that's loaded on the first page visit. * @@ -47,6 +53,8 @@ public function share(Request $request): array 'error' => $request->session()->get('error'), 'loginRequired' => $request->session()->get('loginRequired'), ], + 'locale' => fn (): string => app()->getLocale(), + 'translations' => fn (): array => $this->translationLoader->load(app()->getLocale()), ]; } } diff --git a/app/Http/Requests/StoreCommentRequest.php b/app/Http/Requests/StoreCommentRequest.php index 053ee6f..1ba57b1 100644 --- a/app/Http/Requests/StoreCommentRequest.php +++ b/app/Http/Requests/StoreCommentRequest.php @@ -27,9 +27,9 @@ public function rules(): array public function messages(): array { return [ - 'body.required' => 'El comentario no puede estar vacío.', - 'body.string' => 'El comentario debe ser texto.', - 'body.max' => 'El comentario no puede exceder los 1000 caracteres.', + 'body.required' => __('projects.comment_form.body_required'), + 'body.string' => __('projects.comment_form.body_string'), + 'body.max' => __('projects.comment_form.body_max'), ]; } } diff --git a/app/Http/Requests/TransitionProjectStageRequest.php b/app/Http/Requests/TransitionProjectStageRequest.php index fb6a676..02d19e7 100644 --- a/app/Http/Requests/TransitionProjectStageRequest.php +++ b/app/Http/Requests/TransitionProjectStageRequest.php @@ -34,13 +34,13 @@ function (string $attribute, mixed $value, Closure $fail) use ($project): void { $target = ProjectStage::tryFrom(is_string($value) ? $value : ''); if ($target === null) { - $fail('La etapa solicitada no existe.'); + $fail(__('projects.stage.unknown_target')); return; } if (! $project->canTransitionTo($target)) { - $fail('Esta transición no está permitida desde el estado actual.'); + $fail(__('projects.stage.illegal_transition')); } }, ], diff --git a/app/Observers/ReactionObserver.php b/app/Observers/ReactionObserver.php index e8e2ec8..7a15a12 100644 --- a/app/Observers/ReactionObserver.php +++ b/app/Observers/ReactionObserver.php @@ -16,7 +16,7 @@ class ReactionObserver public function created(Reaction $reaction): void { - if ($reaction->type !== ReactionType::Potenciar) { + if ($reaction->type !== ReactionType::Endorse) { return; } diff --git a/app/ProjectStage.php b/app/ProjectStage.php index 7876315..06c1516 100644 --- a/app/ProjectStage.php +++ b/app/ProjectStage.php @@ -28,11 +28,6 @@ public function canTransitionTo(self $stage): bool public function label(): string { - return match ($this) { - self::Planning => 'Planificación', - self::InExecution => 'En ejecución', - self::Completed => 'Completado', - self::Aborted => 'Abortado', - }; + return __('projects.stage.label.'.$this->value); } } diff --git a/app/ReactionType.php b/app/ReactionType.php index 71261f8..5206f6d 100644 --- a/app/ReactionType.php +++ b/app/ReactionType.php @@ -10,5 +10,5 @@ enum ReactionType: string case Sad = 'sad'; case Thumbsy = 'thumbsy'; case Like = 'like'; - case Potenciar = 'potenciar'; + case Endorse = 'potenciar'; } diff --git a/app/Support/I18n/TranslationLoader.php b/app/Support/I18n/TranslationLoader.php new file mode 100644 index 0000000..bdaef86 --- /dev/null +++ b/app/Support/I18n/TranslationLoader.php @@ -0,0 +1,44 @@ +/ as a domain-keyed + * associative array suitable for the Inertia shared-prop pipeline. + * + * @return array> + */ + public function load(string $locale): array + { + $directory = base_path('lang/'.$locale); + + if (! File::isDirectory($directory)) { + return []; + } + + $payload = []; + + foreach (File::files($directory) as $file) { + if ($file->getExtension() !== 'php') { + continue; + } + + $domain = $file->getFilenameWithoutExtension(); + $contents = require $file->getRealPath(); + + if (is_array($contents)) { + $payload[$domain] = $contents; + } + } + + ksort($payload); + + return $payload; + } +} diff --git a/config/app.php b/config/app.php index 423eed5..cebda83 100644 --- a/config/app.php +++ b/config/app.php @@ -78,11 +78,11 @@ | */ - 'locale' => env('APP_LOCALE', 'en'), + 'locale' => env('APP_LOCALE', 'es'), - 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'es'), - 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + 'faker_locale' => env('APP_FAKER_LOCALE', 'es_MX'), /* |-------------------------------------------------------------------------- diff --git a/lang/en/accessibility.php b/lang/en/accessibility.php new file mode 100644 index 0000000..9653fa7 --- /dev/null +++ b/lang/en/accessibility.php @@ -0,0 +1,30 @@ + [ + 'label' => '', + 'more' => '', + ], + 'dialog' => [ + 'close' => '', + ], + 'sheet' => [ + 'close' => '', + ], + 'sidebar' => [ + 'title' => '', + 'description' => '', + 'toggle' => '', + ], + 'spinner' => [ + 'loading' => '', + ], + 'three_column' => [ + 'company_logo_alt' => '', + ], + 'settings' => [ + 'nav_label' => '', + ], +]; diff --git a/lang/en/auth.php b/lang/en/auth.php index 88da728..3a24f6b 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -1,20 +1,146 @@ 'Las credenciales ingresadas no coinciden con nuestros registros.', - 'password' => 'La contraseña proporcionada es incorrecta.', - 'throttle' => 'Demasiados intentos de inicio de sesión. Por favor intenta de nuevo en :seconds segundos.', +declare(strict_types=1); +return [ + 'ui' => [ + 'shared' => [ + 'email_label' => '', + 'email_placeholder' => '', + 'password_label' => '', + 'password_placeholder' => '', + ], + 'login' => [ + 'title' => '', + 'description' => '', + 'head_title' => '', + 'email_label' => '', + 'email_placeholder' => '', + 'password_label' => '', + 'password_placeholder' => '', + 'remember_label' => '', + 'or_continue_with' => '', + 'forgot_password' => '', + 'submit' => '', + 'continue_google' => '', + 'no_account_prompt' => '', + 'sign_up' => '', + ], + 'register' => [ + 'title' => '', + 'description' => '', + 'head_title' => '', + 'name_label' => '', + 'name_placeholder' => '', + 'email_label' => '', + 'email_placeholder' => '', + 'password_label' => '', + 'password_placeholder' => '', + 'password_confirmation_label' => '', + 'password_confirmation_placeholder' => '', + 'submit' => '', + 'have_account_prompt' => '', + 'log_in' => '', + ], + 'forgot_password' => [ + 'title' => '', + 'description' => '', + 'head_title' => '', + 'email_label' => '', + 'email_placeholder' => '', + 'or_return_to' => '', + 'log_in' => '', + 'submit' => '', + ], + 'reset_password' => [ + 'title' => '', + 'description' => '', + 'head_title' => '', + 'email_label' => '', + 'password_label' => '', + 'password_placeholder' => '', + 'password_confirmation_label' => '', + 'password_confirmation_placeholder' => '', + 'submit' => '', + ], + 'confirm_password' => [ + 'title' => '', + 'description' => '', + 'head_title' => '', + 'password_label' => '', + 'password_placeholder' => '', + 'submit' => '', + ], + 'two_factor_challenge' => [ + 'head_title' => '', + 'recovery_code_placeholder' => '', + 'or_you_can' => '', + 'auth_title' => '', + 'auth_description' => '', + 'recovery_title' => '', + 'recovery_description' => '', + 'toggle_to_recovery' => '', + 'toggle_to_auth' => '', + 'continue' => '', + ], + 'verify_email' => [ + 'title' => '', + 'description' => '', + 'head_title' => '', + 'link_sent' => '', + 'resend' => '', + 'log_out' => '', + ], + 'two_factor_recovery_codes' => [ + 'a11y' => [ + 'recovery_codes' => '', + 'loading_recovery_codes' => '', + ], + 'title' => '', + 'description' => '', + 'hide' => '', + 'view' => '', + 'codes_button_suffix' => '', + 'regenerate' => '', + 'usage_warning_prefix' => '', + 'usage_warning_suffix' => '', + ], + 'two_factor_setup_modal' => [ + 'enabled_title' => '', + 'enabled_description' => '', + 'close' => '', + 'verify_title' => '', + 'verify_description' => '', + 'continue' => '', + 'enable_title' => '', + 'enable_description' => '', + 'manual_code_separator' => '', + 'back' => '', + 'confirm' => '', + ], + 'login_modal' => [ + 'title' => '', + 'description' => '', + 'continue_email' => '', + 'continue_google' => '', + 'log_in' => '', + 'credentials_description' => '', + 'no_account_prompt' => '', + 'sign_up' => '', + 'register_title' => '', + 'register_description' => '', + 'create_account' => '', + 'have_account_prompt' => '', + 'sign_in' => '', + 'name_label' => '', + 'name_placeholder' => '', + 'remember_label' => '', + 'password_confirmation_label' => '', + 'password_confirmation_placeholder' => '', + ], + 'popup_callback' => [ + 'page_title' => '', + 'success_message' => '', + ], + ], ]; diff --git a/lang/en/canary.php b/lang/en/canary.php new file mode 100644 index 0000000..150c213 --- /dev/null +++ b/lang/en/canary.php @@ -0,0 +1,8 @@ + '', + 'items' => '', +]; diff --git a/lang/en/common.php b/lang/en/common.php new file mode 100644 index 0000000..0dae23d --- /dev/null +++ b/lang/en/common.php @@ -0,0 +1,5 @@ + [ + 'view_notifications' => '', + 'your_profile' => '', + ], + 'settings' => [ + 'title' => '', + 'description' => '', + 'nav' => [ + 'profile' => '', + 'password' => '', + 'two_factor' => '', + 'appearance' => '', + ], + ], + 'user_menu' => [ + 'settings' => '', + 'log_out' => '', + ], + 'nav' => [ + 'platform' => '', + ], + 'welcome_sidebar' => [ + 'home' => '', + 'explore' => '', + 'notifications' => '', + 'messages' => '', + 'bookmarks' => '', + 'profile' => '', + 'settings' => '', + 'communities' => '', + 'new_post' => '', + 'log_in' => '', + ], + 'cover_photo' => [ + 'reposition' => '', + 'uploading' => '', + 'edit_cover' => '', + 'cancel' => '', + 'saving' => '', + 'save' => '', + 'drag_hint' => '', + ], + 'header' => [ + 'a11y_navigation_menu' => '', + 'nav_dashboard' => '', + 'nav_repository' => '', + 'nav_documentation' => '', + ], + 'mobile_sidebar' => [ + 'close_menu' => '', + ], + 'examples' => [ + 'bento' => [ + 'releases' => '', + 'push_to_deploy' => '', + 'integrations' => '', + 'security' => '', + 'performance' => '', + ], + 'combobox' => [ + 'assigned_to' => '', + ], + 'profile_heading' => [ + 'message' => '', + 'call' => '', + ], + ], +]; diff --git a/lang/en/mailers.php b/lang/en/mailers.php new file mode 100644 index 0000000..0dae23d --- /dev/null +++ b/lang/en/mailers.php @@ -0,0 +1,5 @@ + '« Previous', - 'next' => 'Next »', - -]; +return []; diff --git a/lang/en/passwords.php b/lang/en/passwords.php index fad3a7d..0dae23d 100644 --- a/lang/en/passwords.php +++ b/lang/en/passwords.php @@ -1,22 +1,5 @@ 'Your password has been reset.', - 'sent' => 'We have emailed your password reset link.', - 'throttled' => 'Please wait before retrying.', - 'token' => 'This password reset token is invalid.', - 'user' => "We can't find a user with that email address.", - -]; +return []; diff --git a/lang/en/profile.php b/lang/en/profile.php new file mode 100644 index 0000000..f0d4907 --- /dev/null +++ b/lang/en/profile.php @@ -0,0 +1,66 @@ + [ + 'a11y' => [ + 'open_menu' => '', + ], + 'member_since' => '', + 'coins' => '', + 'post_singular' => '', + 'post_plural' => '', + 'tabs' => [ + 'posts' => '', + 'talentos' => '', + 'informacion' => '', + ], + ], + 'informacion' => [ + 'bio' => '', + 'bio_placeholder' => '', + 'location' => '', + 'birthdate' => '', + 'public_phone' => '', + 'public_phone_placeholder' => '', + 'contact_email' => '', + 'contact_email_placeholder' => '', + 'languages' => '', + 'empty_placeholder' => '', + 'edit' => '', + 'save' => '', + 'cancel' => '', + ], + 'language_tag' => [ + 'add_language_placeholder' => '', + ], + 'google_places' => [ + 'search_placeholder' => '', + ], + 'experience_slider' => [ + 'less_than_one_year' => '', + 'more_than_one_year' => '', + 'more_than_three_years' => '', + 'more_than_five_years' => '', + 'more_than_ten_years' => '', + ], + 'posts' => [ + 'empty_state' => '', + 'empty_state_body' => '', + ], + 'avatar' => [ + 'view' => '', + 'uploading' => '', + 'update' => '', + ], + 'talentos' => [ + 'empty_state' => '', + 'search_occupation_placeholder' => '', + 'confidence' => [ + 'aprendiz' => '', + 'autosuficiente' => '', + 'maestro' => '', + ], + ], +]; diff --git a/lang/en/projects.php b/lang/en/projects.php new file mode 100644 index 0000000..8d65c43 --- /dev/null +++ b/lang/en/projects.php @@ -0,0 +1,101 @@ + [ + 'open_menu' => '', + 'title' => '', + 'activity' => '', + 'comments' => '', + ], + 'dashboard' => [ + 'title' => '', + ], + 'welcome' => [ + 'open_menu' => '', + 'home' => '', + 'search_placeholder' => '', + 'trending' => '', + 'who_to_follow' => '', + 'see_more' => '', + 'follow' => '', + 'following' => '', + 'footer' => '', + ], + 'header' => [ + 'title_placeholder' => '', + 'description_placeholder' => '', + ], + 'volunteers' => [ + 'applications_closed' => '', + ], + 'stage' => [ + 'unknown_target' => '', + 'illegal_transition' => '', + 'aria_label' => '', + 'label' => [ + 'planning' => '', + 'in_execution' => '', + 'completed' => '', + 'aborted' => '', + ], + ], + 'roles' => [ + 'title_placeholder' => '', + 'description_placeholder' => '', + 'slots' => '', + 'hours_estimated' => '', + 'application_dialog_title' => '', + 'pending_application' => '', + 'looking_for_roles' => '', + 'member' => '', + 'pending_approval_suffix' => '', + ], + 'team' => [ + 'title' => '', + ], + 'gallery' => [ + 'image_title_placeholder' => '', + 'image_description_placeholder' => '', + ], + 'timeline_entry' => [ + 'milestone' => '', + 'status_update' => '', + 'stage_transition_separator' => '', + ], + 'timeline_post_update' => [ + 'milestone_placeholder' => '', + 'status_placeholder' => '', + ], + 'post_actions' => [ + 'create_project' => '', + 'view_project' => '', + ], + 'comment_form' => [ + 'body_placeholder' => '', + 'body_required' => '', + 'body_string' => '', + 'body_max' => '', + 'login_cta' => '', + 'login_suffix' => '', + 'publish' => '', + ], + 'comment_textarea' => [ + 'body_placeholder' => '', + 'submit' => '', + 'a11y' => [ + 'attach_file' => '', + 'your_mood' => '', + 'add_your_mood' => '', + ], + 'moods' => [ + 'excited' => '', + 'loved' => '', + 'happy' => '', + 'sad' => '', + 'thumbsy' => '', + 'none' => '', + ], + ], +]; diff --git a/lang/en/settings.php b/lang/en/settings.php new file mode 100644 index 0000000..b052b0e --- /dev/null +++ b/lang/en/settings.php @@ -0,0 +1,225 @@ + [ + 'perfil' => '', + 'seguridad' => '', + 'notificaciones' => '', + 'privacidad' => '', + ], + 'row' => [ + 'edit_default' => '', + ], + 'configuracion' => [ + 'a11y' => [ + 'open_menu' => '', + 'account_settings' => '', + ], + 'heading' => '', + ], + 'profile' => [ + 'breadcrumb' => '', + 'head_title' => '', + 'a11y' => [ + 'heading' => '', + ], + 'information' => [ + 'title' => '', + 'description' => '', + ], + 'fields' => [ + 'name' => '', + 'name_placeholder' => '', + 'email' => '', + 'email_placeholder' => '', + ], + 'verification' => [ + 'unverified_prefix' => '', + 'resend_link' => '', + 'sent' => '', + ], + 'save_button' => '', + 'saved' => '', + ], + 'password' => [ + 'breadcrumb' => '', + 'head_title' => '', + 'a11y' => [ + 'heading' => '', + ], + 'update' => [ + 'title' => '', + 'description' => '', + ], + 'fields' => [ + 'current' => '', + 'current_placeholder' => '', + 'new' => '', + 'new_placeholder' => '', + 'confirm' => '', + 'confirm_placeholder' => '', + ], + 'save_button' => '', + 'saved' => '', + ], + 'two_factor' => [ + 'breadcrumb' => '', + 'head_title' => '', + 'a11y' => [ + 'heading' => '', + ], + 'section' => [ + 'title' => '', + 'description' => '', + ], + 'enabled_badge' => '', + 'disabled_badge' => '', + 'enabled_description' => '', + 'disabled_description' => '', + 'disable_button' => '', + 'continue_setup_button' => '', + 'enable_button' => '', + 'errors' => [ + 'qr_code' => '', + 'setup_key' => '', + 'recovery_codes' => '', + ], + ], + 'appearance' => [ + 'breadcrumb' => '', + 'head_title' => '', + 'a11y' => [ + 'heading' => '', + ], + 'section' => [ + 'title' => '', + 'description' => '', + ], + 'tabs' => [ + 'light' => '', + 'dark' => '', + 'system' => '', + ], + ], + 'avatar_upload' => [ + 'crop_dialog' => [ + 'title' => '', + 'description' => '', + ], + 'a11y' => [ + 'crop_image_alt' => '', + ], + 'change_photo' => '', + 'help_text' => '', + 'cancel' => '', + 'save' => '', + 'saving' => '', + ], + 'perfil' => [ + 'photo' => [ + 'title' => '', + 'description' => '', + ], + 'personal' => [ + 'title' => '', + 'description' => '', + ], + 'fields' => [ + 'name' => '', + 'email' => '', + 'username' => '', + 'description' => '', + 'description_placeholder' => '', + ], + ], + 'seguridad' => [ + 'password' => [ + 'title' => '', + 'description' => '', + 'label' => '', + 'edit_label' => '', + ], + 'sessions' => [ + 'title' => '', + 'description' => '', + 'logout_button' => '', + ], + ], + 'notificaciones' => [ + 'title' => '', + 'description' => '', + 'followers' => [ + 'label' => '', + 'description' => '', + ], + 'mentions' => [ + 'label' => '', + 'description' => '', + ], + 'comments' => [ + 'label' => '', + 'description' => '', + ], + 'likes' => [ + 'label' => '', + 'description' => '', + ], + 'updates' => [ + 'label' => '', + 'description' => '', + ], + ], + 'privacidad' => [ + 'visibility' => [ + 'title' => '', + 'description' => '', + ], + 'private_account' => [ + 'label' => '', + 'description' => '', + ], + 'show_email' => [ + 'label' => '', + 'description' => '', + ], + 'allow_dms' => [ + 'label' => '', + 'description' => '', + ], + 'data' => [ + 'title' => '', + 'description' => '', + ], + 'download' => [ + 'label' => '', + 'value' => '', + 'edit_label' => '', + ], + 'delete' => [ + 'label' => '', + 'value' => '', + 'edit_label' => '', + ], + ], + 'delete_user' => [ + 'heading' => [ + 'title' => '', + 'description' => '', + ], + 'warning' => '', + 'warning_body' => '', + 'trigger_button' => '', + 'dialog' => [ + 'title' => '', + 'description' => '', + ], + 'fields' => [ + 'password' => '', + 'password_placeholder' => '', + ], + 'cancel_button' => '', + 'confirm_button' => '', + ], +]; diff --git a/lang/en/validation.php b/lang/en/validation.php index 63ec29a..0dae23d 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -1,200 +1,5 @@ 'The :attribute field must be accepted.', - 'accepted_if' => 'The :attribute field must be accepted when :other is :value.', - 'active_url' => 'The :attribute field must be a valid URL.', - 'after' => 'The :attribute field must be a date after :date.', - 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.', - 'alpha' => 'The :attribute field must only contain letters.', - 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.', - 'alpha_num' => 'The :attribute field must only contain letters and numbers.', - 'any_of' => 'The :attribute field is invalid.', - 'array' => 'The :attribute field must be an array.', - 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', - 'before' => 'The :attribute field must be a date before :date.', - 'before_or_equal' => 'The :attribute field must be a date before or equal to :date.', - 'between' => [ - 'array' => 'The :attribute field must have between :min and :max items.', - 'file' => 'The :attribute field must be between :min and :max kilobytes.', - 'numeric' => 'The :attribute field must be between :min and :max.', - 'string' => 'The :attribute field must be between :min and :max characters.', - ], - 'boolean' => 'The :attribute field must be true or false.', - 'can' => 'The :attribute field contains an unauthorized value.', - 'confirmed' => 'The :attribute field confirmation does not match.', - 'contains' => 'The :attribute field is missing a required value.', - 'current_password' => 'The password is incorrect.', - 'date' => 'The :attribute field must be a valid date.', - 'date_equals' => 'The :attribute field must be a date equal to :date.', - 'date_format' => 'The :attribute field must match the format :format.', - 'decimal' => 'The :attribute field must have :decimal decimal places.', - 'declined' => 'The :attribute field must be declined.', - 'declined_if' => 'The :attribute field must be declined when :other is :value.', - 'different' => 'The :attribute field and :other must be different.', - 'digits' => 'The :attribute field must be :digits digits.', - 'digits_between' => 'The :attribute field must be between :min and :max digits.', - 'dimensions' => 'The :attribute field has invalid image dimensions.', - 'distinct' => 'The :attribute field has a duplicate value.', - 'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.', - 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.', - 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.', - 'email' => 'The :attribute field must be a valid email address.', - 'encoding' => 'The :attribute field must be encoded in :encoding.', - 'ends_with' => 'The :attribute field must end with one of the following: :values.', - 'enum' => 'The selected :attribute is invalid.', - 'exists' => 'The selected :attribute is invalid.', - 'extensions' => 'The :attribute field must have one of the following extensions: :values.', - 'file' => 'The :attribute field must be a file.', - 'filled' => 'The :attribute field must have a value.', - 'gt' => [ - 'array' => 'The :attribute field must have more than :value items.', - 'file' => 'The :attribute field must be greater than :value kilobytes.', - 'numeric' => 'The :attribute field must be greater than :value.', - 'string' => 'The :attribute field must be greater than :value characters.', - ], - 'gte' => [ - 'array' => 'The :attribute field must have :value items or more.', - 'file' => 'The :attribute field must be greater than or equal to :value kilobytes.', - 'numeric' => 'The :attribute field must be greater than or equal to :value.', - 'string' => 'The :attribute field must be greater than or equal to :value characters.', - ], - 'hex_color' => 'The :attribute field must be a valid hexadecimal color.', - 'image' => 'The :attribute field must be an image.', - 'in' => 'The selected :attribute is invalid.', - 'in_array' => 'The :attribute field must exist in :other.', - 'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.', - 'integer' => 'The :attribute field must be an integer.', - 'ip' => 'The :attribute field must be a valid IP address.', - 'ipv4' => 'The :attribute field must be a valid IPv4 address.', - 'ipv6' => 'The :attribute field must be a valid IPv6 address.', - 'json' => 'The :attribute field must be a valid JSON string.', - 'list' => 'The :attribute field must be a list.', - 'lowercase' => 'The :attribute field must be lowercase.', - 'lt' => [ - 'array' => 'The :attribute field must have less than :value items.', - 'file' => 'The :attribute field must be less than :value kilobytes.', - 'numeric' => 'The :attribute field must be less than :value.', - 'string' => 'The :attribute field must be less than :value characters.', - ], - 'lte' => [ - 'array' => 'The :attribute field must not have more than :value items.', - 'file' => 'The :attribute field must be less than or equal to :value kilobytes.', - 'numeric' => 'The :attribute field must be less than or equal to :value.', - 'string' => 'The :attribute field must be less than or equal to :value characters.', - ], - 'mac_address' => 'The :attribute field must be a valid MAC address.', - 'max' => [ - 'array' => 'The :attribute field must not have more than :max items.', - 'file' => 'The :attribute field must not be greater than :max kilobytes.', - 'numeric' => 'The :attribute field must not be greater than :max.', - 'string' => 'The :attribute field must not be greater than :max characters.', - ], - 'max_digits' => 'The :attribute field must not have more than :max digits.', - 'mimes' => 'The :attribute field must be a file of type: :values.', - 'mimetypes' => 'The :attribute field must be a file of type: :values.', - 'min' => [ - 'array' => 'The :attribute field must have at least :min items.', - 'file' => 'The :attribute field must be at least :min kilobytes.', - 'numeric' => 'The :attribute field must be at least :min.', - 'string' => 'The :attribute field must be at least :min characters.', - ], - 'min_digits' => 'The :attribute field must have at least :min digits.', - 'missing' => 'The :attribute field must be missing.', - 'missing_if' => 'The :attribute field must be missing when :other is :value.', - 'missing_unless' => 'The :attribute field must be missing unless :other is :value.', - 'missing_with' => 'The :attribute field must be missing when :values is present.', - 'missing_with_all' => 'The :attribute field must be missing when :values are present.', - 'multiple_of' => 'The :attribute field must be a multiple of :value.', - 'not_in' => 'The selected :attribute is invalid.', - 'not_regex' => 'The :attribute field format is invalid.', - 'numeric' => 'The :attribute field must be a number.', - 'password' => [ - 'letters' => 'The :attribute field must contain at least one letter.', - 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', - 'numbers' => 'The :attribute field must contain at least one number.', - 'symbols' => 'The :attribute field must contain at least one symbol.', - 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.', - ], - 'present' => 'The :attribute field must be present.', - 'present_if' => 'The :attribute field must be present when :other is :value.', - 'present_unless' => 'The :attribute field must be present unless :other is :value.', - 'present_with' => 'The :attribute field must be present when :values is present.', - 'present_with_all' => 'The :attribute field must be present when :values are present.', - 'prohibited' => 'The :attribute field is prohibited.', - 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', - 'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.', - 'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.', - 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', - 'prohibits' => 'The :attribute field prohibits :other from being present.', - 'regex' => 'The :attribute field format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_array_keys' => 'The :attribute field must contain entries for: :values.', - 'required_if' => 'The :attribute field is required when :other is :value.', - 'required_if_accepted' => 'The :attribute field is required when :other is accepted.', - 'required_if_declined' => 'The :attribute field is required when :other is declined.', - 'required_unless' => 'The :attribute field is required unless :other is in :values.', - 'required_with' => 'The :attribute field is required when :values is present.', - 'required_with_all' => 'The :attribute field is required when :values are present.', - 'required_without' => 'The :attribute field is required when :values is not present.', - 'required_without_all' => 'The :attribute field is required when none of :values are present.', - 'same' => 'The :attribute field must match :other.', - 'size' => [ - 'array' => 'The :attribute field must contain :size items.', - 'file' => 'The :attribute field must be :size kilobytes.', - 'numeric' => 'The :attribute field must be :size.', - 'string' => 'The :attribute field must be :size characters.', - ], - 'starts_with' => 'The :attribute field must start with one of the following: :values.', - 'string' => 'The :attribute field must be a string.', - 'timezone' => 'The :attribute field must be a valid timezone.', - 'unique' => 'The :attribute has already been taken.', - 'uploaded' => 'The :attribute failed to upload.', - 'uppercase' => 'The :attribute field must be uppercase.', - 'url' => 'The :attribute field must be a valid URL.', - 'ulid' => 'The :attribute field must be a valid ULID.', - 'uuid' => 'The :attribute field must be a valid UUID.', - - /* - |-------------------------------------------------------------------------- - | Custom Validation Language Lines - |-------------------------------------------------------------------------- - | - | Here you may specify custom validation messages for attributes using the - | convention "attribute.rule" to name the lines. This makes it quick to - | specify a specific custom language line for a given attribute rule. - | - */ - - 'custom' => [ - 'attribute-name' => [ - 'rule-name' => 'custom-message', - ], - ], - - /* - |-------------------------------------------------------------------------- - | Custom Validation Attributes - |-------------------------------------------------------------------------- - | - | The following language lines are used to swap our attribute placeholder - | with something more reader friendly such as "E-Mail Address" instead - | of "email". This simply helps us make our message more expressive. - | - */ - - 'attributes' => [], - -]; +return []; diff --git a/lang/es/accessibility.php b/lang/es/accessibility.php new file mode 100644 index 0000000..1dd63d7 --- /dev/null +++ b/lang/es/accessibility.php @@ -0,0 +1,30 @@ + [ + 'label' => 'breadcrumb', + 'more' => 'More', + ], + 'dialog' => [ + 'close' => 'Close', + ], + 'sheet' => [ + 'close' => 'Close', + ], + 'sidebar' => [ + 'title' => 'Sidebar', + 'description' => 'Displays the mobile sidebar.', + 'toggle' => 'Toggle Sidebar', + ], + 'spinner' => [ + 'loading' => 'Loading', + ], + 'three_column' => [ + 'company_logo_alt' => 'Your Company', + ], + 'settings' => [ + 'nav_label' => 'Settings', + ], +]; diff --git a/lang/es/auth.php b/lang/es/auth.php new file mode 100644 index 0000000..57111f2 --- /dev/null +++ b/lang/es/auth.php @@ -0,0 +1,146 @@ + [ + 'shared' => [ + 'email_label' => 'Correo electrónico', + 'email_placeholder' => 'correo@ejemplo.com', + 'password_label' => 'Contraseña', + 'password_placeholder' => 'Contraseña', + ], + 'login' => [ + 'title' => 'Inicia sesión en tu cuenta', + 'description' => 'Ingresa tu correo y contraseña para iniciar sesión', + 'head_title' => 'Iniciar sesión', + 'email_label' => 'Correo electrónico', + 'email_placeholder' => 'correo@ejemplo.com', + 'password_label' => 'Contraseña', + 'password_placeholder' => 'Contraseña', + 'remember_label' => 'Recordarme', + 'or_continue_with' => 'O continúa con', + 'forgot_password' => '¿Olvidaste tu contraseña?', + 'submit' => 'Iniciar sesión', + 'continue_google' => 'Iniciar sesión con Google', + 'no_account_prompt' => '¿No tienes cuenta?', + 'sign_up' => 'Regístrate', + ], + 'register' => [ + 'title' => 'Crea una cuenta', + 'description' => 'Ingresa tus datos para crear tu cuenta', + 'head_title' => 'Registrarse', + 'name_label' => 'Nombre', + 'name_placeholder' => 'Nombre completo', + 'email_label' => 'Correo electrónico', + 'email_placeholder' => 'correo@ejemplo.com', + 'password_label' => 'Contraseña', + 'password_placeholder' => 'Contraseña', + 'password_confirmation_label' => 'Confirmar contraseña', + 'password_confirmation_placeholder' => 'Confirmar contraseña', + 'submit' => 'Crear cuenta', + 'have_account_prompt' => '¿Ya tienes cuenta?', + 'log_in' => 'Iniciar sesión', + ], + 'forgot_password' => [ + 'title' => 'Recuperar contraseña', + 'description' => 'Ingresa tu correo para recibir un enlace de restablecimiento', + 'head_title' => 'Recuperar contraseña', + 'email_label' => 'Correo electrónico', + 'email_placeholder' => 'correo@ejemplo.com', + 'or_return_to' => 'O regresa a', + 'log_in' => 'iniciar sesión', + 'submit' => 'Enviar enlace de restablecimiento', + ], + 'reset_password' => [ + 'title' => 'Restablecer contraseña', + 'description' => 'Por favor ingresa tu nueva contraseña', + 'head_title' => 'Restablecer contraseña', + 'email_label' => 'Correo electrónico', + 'password_label' => 'Contraseña', + 'password_placeholder' => 'Contraseña', + 'password_confirmation_label' => 'Confirmar contraseña', + 'password_confirmation_placeholder' => 'Confirmar contraseña', + 'submit' => 'Restablecer contraseña', + ], + 'confirm_password' => [ + 'title' => 'Confirma tu contraseña', + 'description' => 'Esta es un área segura de la aplicación. Por favor confirma tu contraseña antes de continuar.', + 'head_title' => 'Confirmar contraseña', + 'password_label' => 'Contraseña', + 'password_placeholder' => 'Contraseña', + 'submit' => 'Confirmar contraseña', + ], + 'two_factor_challenge' => [ + 'head_title' => 'Autenticación de dos factores', + 'recovery_code_placeholder' => 'Ingresa el código de recuperación', + 'or_you_can' => 'o puedes', + 'auth_title' => 'Código de autenticación', + 'auth_description' => 'Ingresa el código de autenticación de tu app autenticadora.', + 'recovery_title' => 'Código de recuperación', + 'recovery_description' => 'Por favor confirma el acceso a tu cuenta ingresando uno de tus códigos de recuperación de emergencia.', + 'toggle_to_recovery' => 'iniciar sesión con un código de recuperación', + 'toggle_to_auth' => 'iniciar sesión con un código de autenticación', + 'continue' => 'Continuar', + ], + 'verify_email' => [ + 'title' => 'Verificar correo', + 'description' => 'Por favor verifica tu correo haciendo clic en el enlace que te enviamos.', + 'head_title' => 'Verificación de correo', + 'link_sent' => 'Se envió un nuevo enlace de verificación al correo proporcionado durante el registro.', + 'resend' => 'Reenviar correo de verificación', + 'log_out' => 'Cerrar sesión', + ], + 'two_factor_recovery_codes' => [ + 'a11y' => [ + 'recovery_codes' => 'Códigos de recuperación', + 'loading_recovery_codes' => 'Cargando códigos de recuperación', + ], + 'title' => 'Códigos de recuperación 2FA', + 'description' => 'Los códigos de recuperación te permiten recuperar el acceso si pierdes tu dispositivo 2FA. Guárdalos en un gestor de contraseñas seguro.', + 'hide' => 'Ocultar', + 'view' => 'Ver', + 'codes_button_suffix' => 'códigos de recuperación', + 'regenerate' => 'Regenerar códigos', + 'usage_warning_prefix' => 'Cada código de recuperación puede usarse una vez para acceder a tu cuenta y se eliminará tras su uso. Si necesitas más, haz clic en', + 'usage_warning_suffix' => 'arriba.', + ], + 'two_factor_setup_modal' => [ + 'enabled_title' => 'Autenticación de dos factores activada', + 'enabled_description' => 'La autenticación de dos factores está activada. Escanea el código QR o ingresa la clave de configuración en tu app autenticadora.', + 'close' => 'Cerrar', + 'verify_title' => 'Verificar código de autenticación', + 'verify_description' => 'Ingresa el código de 6 dígitos de tu app autenticadora', + 'continue' => 'Continuar', + 'enable_title' => 'Activar autenticación de dos factores', + 'enable_description' => 'Para terminar de activar la autenticación de dos factores, escanea el código QR o ingresa la clave de configuración en tu app autenticadora', + 'manual_code_separator' => 'o ingresa el código manualmente', + 'back' => 'Atrás', + 'confirm' => 'Confirmar', + ], + 'login_modal' => [ + 'title' => 'Inicia sesión', + 'description' => 'Necesitas iniciar sesión para realizar esta acción.', + 'continue_email' => 'Continuar con correo y contraseña', + 'continue_google' => 'Continuar con Google', + 'log_in' => 'Iniciar sesión', + 'credentials_description' => 'Ingresa tu correo y contraseña para continuar.', + 'no_account_prompt' => '¿No tienes cuenta?', + 'sign_up' => 'Regístrate', + 'register_title' => 'Crear cuenta', + 'register_description' => 'Completa los datos para registrarte.', + 'create_account' => 'Crear cuenta', + 'have_account_prompt' => '¿Ya tienes cuenta?', + 'sign_in' => 'Inicia sesión', + 'name_label' => 'Nombre', + 'name_placeholder' => 'Tu nombre', + 'remember_label' => 'Recordarme', + 'password_confirmation_label' => 'Confirmar contraseña', + 'password_confirmation_placeholder' => 'Repite la contraseña', + ], + 'popup_callback' => [ + 'page_title' => 'Autenticación exitosa', + 'success_message' => 'Autenticación exitosa. Puedes cerrar esta ventana.', + ], + ], +]; diff --git a/lang/es/canary.php b/lang/es/canary.php new file mode 100644 index 0000000..c5d8568 --- /dev/null +++ b/lang/es/canary.php @@ -0,0 +1,8 @@ + 'Hola, :name.', + 'items' => 'Un elemento|:count elementos', +]; diff --git a/lang/es/common.php b/lang/es/common.php new file mode 100644 index 0000000..0dae23d --- /dev/null +++ b/lang/es/common.php @@ -0,0 +1,5 @@ + [ + 'view_notifications' => 'View notifications', + 'your_profile' => 'Your profile', + ], + 'settings' => [ + 'title' => 'Configuración', + 'description' => 'Administra tu perfil y la configuración de tu cuenta', + 'nav' => [ + 'profile' => 'Perfil', + 'password' => 'Contraseña', + 'two_factor' => 'Autenticación de dos factores', + 'appearance' => 'Apariencia', + ], + ], + 'user_menu' => [ + 'settings' => 'Configuración', + 'log_out' => 'Cerrar sesión', + ], + 'nav' => [ + 'platform' => 'Plataforma', + ], + 'welcome_sidebar' => [ + 'home' => 'Inicio', + 'explore' => 'Explorar', + 'notifications' => 'Notificaciones', + 'messages' => 'Mensajes', + 'bookmarks' => 'Guardados', + 'profile' => 'Perfil', + 'settings' => 'Configuración', + 'communities' => 'Comunidades', + 'new_post' => 'Nuevo post', + 'log_in' => 'Iniciar sesión', + ], + 'cover_photo' => [ + 'reposition' => 'Reposicionar', + 'uploading' => 'Subiendo...', + 'edit_cover' => 'Editar portada', + 'cancel' => 'Cancelar', + 'saving' => 'Guardando...', + 'save' => 'Guardar', + 'drag_hint' => 'Arrastra para reposicionar', + ], + 'header' => [ + 'a11y_navigation_menu' => 'Navigation Menu', + 'nav_dashboard' => 'Dashboard', + 'nav_repository' => 'Repository', + 'nav_documentation' => 'Documentation', + ], + 'mobile_sidebar' => [ + 'close_menu' => 'Cerrar menú', + ], + 'examples' => [ + 'bento' => [ + 'releases' => 'Releases', + 'push_to_deploy' => 'Push to deploy', + 'integrations' => 'Integrations', + 'security' => 'Security', + 'performance' => 'Performance', + ], + 'combobox' => [ + 'assigned_to' => 'Assigned to', + ], + 'profile_heading' => [ + 'message' => 'Message', + 'call' => 'Call', + ], + ], +]; diff --git a/lang/es/mailers.php b/lang/es/mailers.php new file mode 100644 index 0000000..0dae23d --- /dev/null +++ b/lang/es/mailers.php @@ -0,0 +1,5 @@ + [ + 'a11y' => [ + 'open_menu' => 'Abrir menú', + ], + 'member_since' => 'Miembro desde', + 'coins' => 'monedas', + 'post_singular' => 'post', + 'post_plural' => 'posts', + 'tabs' => [ + 'posts' => 'Posts', + 'talentos' => 'Talentos', + 'informacion' => 'Información', + ], + ], + 'informacion' => [ + 'bio' => 'Bio', + 'bio_placeholder' => 'Cuéntanos sobre ti...', + 'location' => 'Ubicación', + 'birthdate' => 'Fecha de nacimiento', + 'public_phone' => 'Teléfono público', + 'public_phone_placeholder' => '+52 33 1234 5678', + 'contact_email' => 'Email de contacto', + 'contact_email_placeholder' => 'contacto@ejemplo.com', + 'languages' => 'Idiomas', + 'empty_placeholder' => 'No hay información para mostrar', + 'edit' => 'Editar', + 'save' => 'Guardar', + 'cancel' => 'Cancelar', + ], + 'language_tag' => [ + 'add_language_placeholder' => 'Agregar idioma...', + ], + 'google_places' => [ + 'search_placeholder' => 'Buscar ubicación...', + ], + 'experience_slider' => [ + 'less_than_one_year' => 'Menos de 1 año', + 'more_than_one_year' => 'Más de un año', + 'more_than_three_years' => 'Más de 3 años', + 'more_than_five_years' => 'Más de 5 años', + 'more_than_ten_years' => 'Más de 10 años', + ], + 'posts' => [ + 'empty_state' => 'Sin publicaciones', + 'empty_state_body' => 'Este usuario aún no ha publicado nada.', + ], + 'avatar' => [ + 'view' => 'Ver', + 'uploading' => 'Subiendo...', + 'update' => 'Actualizar', + ], + 'talentos' => [ + 'empty_state' => 'Sin talentos', + 'search_occupation_placeholder' => 'Buscar ocupación...', + 'confidence' => [ + 'aprendiz' => 'Aprendiz', + 'autosuficiente' => 'Autosuficiente', + 'maestro' => 'Maestro', + ], + ], +]; diff --git a/lang/es/projects.php b/lang/es/projects.php new file mode 100644 index 0000000..f998be2 --- /dev/null +++ b/lang/es/projects.php @@ -0,0 +1,101 @@ + [ + 'open_menu' => 'Abrir menú', + 'title' => 'Proyecto', + 'activity' => 'Actividad del proyecto', + 'comments' => 'Comentarios', + ], + 'dashboard' => [ + 'title' => 'Dashboard', + ], + 'welcome' => [ + 'open_menu' => 'Abrir menú', + 'home' => 'Inicio', + 'search_placeholder' => 'Buscar', + 'trending' => 'Tendencias', + 'who_to_follow' => 'A quién seguir', + 'see_more' => 'Ver más', + 'follow' => 'Seguir', + 'following' => 'Siguiendo', + 'footer' => '© 2026 Tekitl · Privacidad · Términos', + ], + 'header' => [ + 'title_placeholder' => 'Título del proyecto', + 'description_placeholder' => 'Descripción del proyecto', + ], + 'volunteers' => [ + 'applications_closed' => 'No se aceptan postulaciones en este momento.', + ], + 'stage' => [ + 'unknown_target' => 'La etapa solicitada no existe.', + 'illegal_transition' => 'Esta transición no está permitida desde el estado actual.', + 'aria_label' => 'Etapa del proyecto: :stage', + 'label' => [ + 'planning' => 'Planificación', + 'in_execution' => 'En ejecución', + 'completed' => 'Completado', + 'aborted' => 'Abortado', + ], + ], + 'roles' => [ + 'title_placeholder' => 'Título del rol *', + 'description_placeholder' => 'Descripción (opcional)', + 'slots' => 'Plazas', + 'hours_estimated' => 'Horas estimadas', + 'application_dialog_title' => 'Solicitud de voluntariado', + 'pending_application' => 'Solicitud pendiente', + 'looking_for_roles' => 'Roles buscados', + 'member' => 'Miembro ✓', + 'pending_approval_suffix' => '— pendiente de aprobación', + ], + 'team' => [ + 'title' => 'Equipo del proyecto', + ], + 'gallery' => [ + 'image_title_placeholder' => 'Título de la imagen', + 'image_description_placeholder' => 'Descripción de la imagen', + ], + 'timeline_entry' => [ + 'milestone' => 'Hito:', + 'status_update' => 'Actualización:', + 'stage_transition_separator' => 'a', + ], + 'timeline_post_update' => [ + 'milestone_placeholder' => '¿Qué hito alcanzaron?', + 'status_placeholder' => 'Compartí una actualización con la comunidad…', + ], + 'post_actions' => [ + 'create_project' => 'Crear proyecto', + 'view_project' => 'Ver proyecto', + ], + 'comment_form' => [ + 'body_placeholder' => 'Escribe un comentario...', + 'body_required' => 'El comentario no puede estar vacío.', + 'body_string' => 'El comentario debe ser texto.', + 'body_max' => 'El comentario no puede exceder los :max caracteres.', + 'login_cta' => 'Inicia sesión', + 'login_suffix' => 'para comentar.', + 'publish' => 'Publicar', + ], + 'comment_textarea' => [ + 'body_placeholder' => '¿Qué tienes en mente?', + 'submit' => 'Publicar', + 'a11y' => [ + 'attach_file' => 'Adjuntar archivo', + 'your_mood' => 'Tu estado de ánimo', + 'add_your_mood' => 'Agregar tu estado de ánimo', + ], + 'moods' => [ + 'excited' => 'Emocionado', + 'loved' => 'Enamorado', + 'happy' => 'Feliz', + 'sad' => 'Triste', + 'thumbsy' => 'Aprobado', + 'none' => 'No siento nada', + ], + ], +]; diff --git a/lang/es/settings.php b/lang/es/settings.php new file mode 100644 index 0000000..a653507 --- /dev/null +++ b/lang/es/settings.php @@ -0,0 +1,225 @@ + [ + 'perfil' => 'Perfil', + 'seguridad' => 'Seguridad', + 'notificaciones' => 'Notificaciones', + 'privacidad' => 'Privacidad', + ], + 'row' => [ + 'edit_default' => 'Actualizar', + ], + 'configuracion' => [ + 'a11y' => [ + 'open_menu' => 'Abrir menú', + 'account_settings' => 'Configuración de la cuenta', + ], + 'heading' => 'Configuración', + ], + 'profile' => [ + 'breadcrumb' => 'Profile settings', + 'head_title' => 'Profile settings', + 'a11y' => [ + 'heading' => 'Profile Settings', + ], + 'information' => [ + 'title' => 'Profile information', + 'description' => 'Update your name and email address', + ], + 'fields' => [ + 'name' => 'Name', + 'name_placeholder' => 'Full name', + 'email' => 'Email address', + 'email_placeholder' => 'Email address', + ], + 'verification' => [ + 'unverified_prefix' => 'Your email address is unverified.', + 'resend_link' => 'Click here to resend the verification email.', + 'sent' => 'A new verification link has been sent to your email address.', + ], + 'save_button' => 'Save', + 'saved' => 'Saved', + ], + 'password' => [ + 'breadcrumb' => 'Password settings', + 'head_title' => 'Password settings', + 'a11y' => [ + 'heading' => 'Password Settings', + ], + 'update' => [ + 'title' => 'Update password', + 'description' => 'Ensure your account is using a long, random password to stay secure', + ], + 'fields' => [ + 'current' => 'Current password', + 'current_placeholder' => 'Current password', + 'new' => 'New password', + 'new_placeholder' => 'New password', + 'confirm' => 'Confirm password', + 'confirm_placeholder' => 'Confirm password', + ], + 'save_button' => 'Save password', + 'saved' => 'Saved', + ], + 'two_factor' => [ + 'breadcrumb' => 'Two-Factor Authentication', + 'head_title' => 'Two-Factor Authentication', + 'a11y' => [ + 'heading' => 'Two-Factor Authentication Settings', + ], + 'section' => [ + 'title' => 'Two-Factor Authentication', + 'description' => 'Manage your two-factor authentication settings', + ], + 'enabled_badge' => 'Enabled', + 'disabled_badge' => 'Disabled', + 'enabled_description' => 'With two-factor authentication enabled, you will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.', + 'disabled_description' => 'When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.', + 'disable_button' => 'Disable 2FA', + 'continue_setup_button' => 'Continue Setup', + 'enable_button' => 'Enable 2FA', + 'errors' => [ + 'qr_code' => 'No se pudo obtener el código QR', + 'setup_key' => 'No se pudo obtener la clave de configuración', + 'recovery_codes' => 'No se pudieron obtener los códigos de recuperación', + ], + ], + 'appearance' => [ + 'breadcrumb' => 'Appearance settings', + 'head_title' => 'Appearance settings', + 'a11y' => [ + 'heading' => 'Appearance Settings', + ], + 'section' => [ + 'title' => 'Appearance settings', + 'description' => "Update your account's appearance settings", + ], + 'tabs' => [ + 'light' => 'Light', + 'dark' => 'Dark', + 'system' => 'System', + ], + ], + 'avatar_upload' => [ + 'crop_dialog' => [ + 'title' => 'Recortar foto', + 'description' => 'Ajusta el recorte de tu foto de perfil.', + ], + 'a11y' => [ + 'crop_image_alt' => 'Recortar', + ], + 'change_photo' => 'Cambiar foto', + 'help_text' => 'JPG, GIF o PNG. 4MB max.', + 'cancel' => 'Cancelar', + 'save' => 'Guardar', + 'saving' => 'Guardando...', + ], + 'perfil' => [ + 'photo' => [ + 'title' => 'Foto de perfil', + 'description' => 'Esta imagen será visible públicamente en tu perfil.', + ], + 'personal' => [ + 'title' => 'Información personal', + 'description' => 'Esta información será visible públicamente en tu perfil.', + ], + 'fields' => [ + 'name' => 'Nombre completo', + 'email' => 'Correo electrónico', + 'username' => 'Nombre de usuario', + 'description' => 'Descripción', + 'description_placeholder' => 'Cuéntanos sobre ti...', + ], + ], + 'seguridad' => [ + 'password' => [ + 'title' => 'Contraseña', + 'description' => 'Actualiza la contraseña asociada a tu cuenta.', + 'label' => 'Contraseña', + 'edit_label' => 'Cambiar', + ], + 'sessions' => [ + 'title' => 'Sesiones activas', + 'description' => 'Cierra sesión en todos los demás dispositivos donde hayas iniciado sesión.', + 'logout_button' => 'Cerrar otras sesiones', + ], + ], + 'notificaciones' => [ + 'title' => 'Notificaciones', + 'description' => 'Elige cómo y cuándo quieres recibir notificaciones.', + 'followers' => [ + 'label' => 'Nuevos seguidores', + 'description' => 'Recibe una notificación cuando alguien empieza a seguirte.', + ], + 'mentions' => [ + 'label' => 'Menciones', + 'description' => 'Recibe una notificación cuando alguien te menciona en un post.', + ], + 'comments' => [ + 'label' => 'Comentarios', + 'description' => 'Recibe una notificación cuando alguien comenta en tus posts.', + ], + 'likes' => [ + 'label' => 'Me gusta', + 'description' => 'Recibe una notificación cuando alguien le da me gusta a tus posts.', + ], + 'updates' => [ + 'label' => 'Actualizaciones de la plataforma', + 'description' => 'Entérate de nuevas funciones y mejoras de Tekitl.', + ], + ], + 'privacidad' => [ + 'visibility' => [ + 'title' => 'Visibilidad de la cuenta', + 'description' => 'Controla quién puede ver tu perfil y tu contenido.', + ], + 'private_account' => [ + 'label' => 'Cuenta privada', + 'description' => 'Solo tus seguidores aprobados podrán ver tus posts.', + ], + 'show_email' => [ + 'label' => 'Mostrar correo electrónico en el perfil', + 'description' => 'Tu correo será visible para otros usuarios.', + ], + 'allow_dms' => [ + 'label' => 'Permitir mensajes directos', + 'description' => 'Cualquier usuario puede enviarte mensajes directos.', + ], + 'data' => [ + 'title' => 'Datos y actividad', + 'description' => 'Gestiona cómo se usan tus datos en la plataforma.', + ], + 'download' => [ + 'label' => 'Descargar mis datos', + 'value' => 'Solicita una copia de toda tu información', + 'edit_label' => 'Solicitar', + ], + 'delete' => [ + 'label' => 'Eliminar cuenta', + 'value' => 'Esta acción es permanente e irreversible', + 'edit_label' => 'Eliminar', + ], + ], + 'delete_user' => [ + 'heading' => [ + 'title' => 'Eliminar cuenta', + 'description' => 'Elimina tu cuenta y todos sus recursos', + ], + 'warning' => 'Advertencia', + 'warning_body' => 'Procede con cuidado, esta acción no puede deshacerse.', + 'trigger_button' => 'Eliminar cuenta', + 'dialog' => [ + 'title' => '¿Estás seguro de que quieres eliminar tu cuenta?', + 'description' => 'Una vez eliminada tu cuenta, todos sus recursos y datos se eliminarán permanentemente. Ingresa tu contraseña para confirmar que deseas eliminar tu cuenta de forma permanente.', + ], + 'fields' => [ + 'password' => 'Contraseña', + 'password_placeholder' => 'Contraseña', + ], + 'cancel_button' => 'Cancelar', + 'confirm_button' => 'Eliminar cuenta', + ], +]; diff --git a/lang/es/validation.php b/lang/es/validation.php new file mode 100644 index 0000000..1f6a841 --- /dev/null +++ b/lang/es/validation.php @@ -0,0 +1,6 @@ +tests/Browser + + + ci-perf + + app diff --git a/resources/js/app.tsx b/resources/js/app.tsx index af55b68..751cb93 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -34,9 +34,15 @@ createInertiaApp({ root.render( - - - + + {({ Component, key, props: pageProps }) => ( + <> + + + + + )} + , ); }, diff --git a/resources/js/components/app-header.tsx b/resources/js/components/app-header.tsx index dc9583c..17c4cb5 100644 --- a/resources/js/components/app-header.tsx +++ b/resources/js/components/app-header.tsx @@ -30,37 +30,17 @@ import { import { UserMenuContent } from '@/components/user-menu-content'; import { useCurrentUrl } from '@/hooks/use-current-url'; import { useInitials } from '@/hooks/use-initials'; +import { t } from '@/lib/i18n'; import { cn, toUrl } from '@/lib/utils'; +import { dashboard } from '@/routes'; import type { BreadcrumbItem, NavItem } from '@/types'; import AppLogo from './app-logo'; import AppLogoIcon from './app-logo-icon'; -import { dashboard } from '@/routes'; type Props = { breadcrumbs?: BreadcrumbItem[]; }; -const mainNavItems: NavItem[] = [ - { - title: 'Dashboard', - href: dashboard(), - icon: LayoutGrid, - }, -]; - -const rightNavItems: NavItem[] = [ - { - title: 'Repository', - href: 'https://github.com/laravel/react-starter-kit', - icon: Folder, - }, - { - title: 'Documentation', - href: 'https://laravel.com/docs/starter-kits#react', - icon: BookOpen, - }, -]; - const activeItemStyles = 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'; @@ -69,6 +49,27 @@ export function AppHeader({ breadcrumbs = [] }: Props) { const { auth } = page.props; const getInitials = useInitials(); const { isCurrentUrl, whenCurrentUrl } = useCurrentUrl(); + + const mainNavItems: NavItem[] = [ + { + title: t('layout.header.nav_dashboard'), + href: dashboard(), + icon: LayoutGrid, + }, + ]; + + const rightNavItems: NavItem[] = [ + { + title: t('layout.header.nav_repository'), + href: 'https://github.com/laravel/react-starter-kit', + icon: Folder, + }, + { + title: t('layout.header.nav_documentation'), + href: 'https://laravel.com/docs/starter-kits#react', + icon: BookOpen, + }, + ]; return ( <>
@@ -90,7 +91,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) { className="flex h-full w-64 flex-col items-stretch justify-between bg-sidebar" > - Navigation Menu + {t('layout.header.a11y_navigation_menu')} diff --git a/resources/js/components/app-logo.tsx b/resources/js/components/app-logo.tsx index 42d998f..b190b4b 100644 --- a/resources/js/components/app-logo.tsx +++ b/resources/js/components/app-logo.tsx @@ -8,7 +8,7 @@ export default function AppLogo() {
- Laravel Starter Kit + Tekitl
diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index ad1b7e5..97d944f 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -12,32 +12,33 @@ import { SidebarMenuButton, SidebarMenuItem, } from '@/components/ui/sidebar'; +import { t } from '@/lib/i18n'; +import { dashboard } from '@/routes'; import type { NavItem } from '@/types'; import AppLogo from './app-logo'; -import { dashboard } from '@/routes'; -const mainNavItems: NavItem[] = [ - { - title: 'Dashboard', - href: dashboard(), - icon: LayoutGrid, - }, -]; +export function AppSidebar() { + const mainNavItems: NavItem[] = [ + { + title: t('layout.header.nav_dashboard'), + href: dashboard(), + icon: LayoutGrid, + }, + ]; -const footerNavItems: NavItem[] = [ - { - title: 'Repository', - href: 'https://github.com/laravel/react-starter-kit', - icon: Folder, - }, - { - title: 'Documentation', - href: 'https://laravel.com/docs/starter-kits#react', - icon: BookOpen, - }, -]; + const footerNavItems: NavItem[] = [ + { + title: t('layout.header.nav_repository'), + href: 'https://github.com/laravel/react-starter-kit', + icon: Folder, + }, + { + title: t('layout.header.nav_documentation'), + href: 'https://laravel.com/docs/starter-kits#react', + icon: BookOpen, + }, + ]; -export function AppSidebar() { return ( diff --git a/resources/js/components/appearance-tabs.tsx b/resources/js/components/appearance-tabs.tsx index b013862..4686079 100644 --- a/resources/js/components/appearance-tabs.tsx +++ b/resources/js/components/appearance-tabs.tsx @@ -3,6 +3,7 @@ import { Monitor, Moon, Sun } from 'lucide-react'; import type { HTMLAttributes } from 'react'; import type { Appearance } from '@/hooks/use-appearance'; import { useAppearance } from '@/hooks/use-appearance'; +import { t } from '@/lib/i18n'; import { cn } from '@/lib/utils'; export default function AppearanceToggleTab({ @@ -12,9 +13,9 @@ export default function AppearanceToggleTab({ const { appearance, updateAppearance } = useAppearance(); const tabs: { value: Appearance; icon: LucideIcon; label: string }[] = [ - { value: 'light', icon: Sun, label: 'Light' }, - { value: 'dark', icon: Moon, label: 'Dark' }, - { value: 'system', icon: Monitor, label: 'System' }, + { value: 'light', icon: Sun, label: t('settings.appearance.tabs.light') }, + { value: 'dark', icon: Moon, label: t('settings.appearance.tabs.dark') }, + { value: 'system', icon: Monitor, label: t('settings.appearance.tabs.system') }, ]; return ( diff --git a/resources/js/components/delete-user.tsx b/resources/js/components/delete-user.tsx index 7ca90e0..4bf307d 100644 --- a/resources/js/components/delete-user.tsx +++ b/resources/js/components/delete-user.tsx @@ -1,5 +1,6 @@ import { Form } from '@inertiajs/react'; import { useRef } from 'react'; +import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; import Heading from '@/components/heading'; import InputError from '@/components/input-error'; import { Button } from '@/components/ui/button'; @@ -14,7 +15,7 @@ import { } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; +import { t } from '@/lib/i18n'; export default function DeleteUser() { const passwordInput = useRef(null); @@ -23,14 +24,14 @@ export default function DeleteUser() {
-

Warning

+

{t('settings.delete_user.warning')}

- Please proceed with caution, this cannot be undone. + {t('settings.delete_user.warning_body')}

@@ -40,18 +41,15 @@ export default function DeleteUser() { variant="destructive" data-test="delete-user-button" > - Delete account + {t('settings.delete_user.trigger_button')} - Are you sure you want to delete your account? + {t('settings.delete_user.dialog.title')} - Once your account is deleted, all of its resources - and data will also be permanently deleted. Please - enter your password to confirm you would like to - permanently delete your account. + {t('settings.delete_user.dialog.description')}
- Password + {t('settings.delete_user.fields.password')} @@ -93,7 +91,7 @@ export default function DeleteUser() { resetAndClearErrors() } > - Cancel + {t('settings.delete_user.cancel_button')} @@ -106,7 +104,7 @@ export default function DeleteUser() { type="submit" data-test="confirm-delete-user-button" > - Delete account + {t('settings.delete_user.confirm_button')} diff --git a/resources/js/components/examples/ComboBox.tsx b/resources/js/components/examples/ComboBox.tsx index 3f32757..5d9687c 100644 --- a/resources/js/components/examples/ComboBox.tsx +++ b/resources/js/components/examples/ComboBox.tsx @@ -1,69 +1,86 @@ -'use client' +'use client'; -import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Label } from '@headlessui/react' -import { ChevronDownIcon } from '@heroicons/react/20/solid' -import { useState } from 'react' +import { + Combobox, + ComboboxButton, + ComboboxInput, + ComboboxOption, + ComboboxOptions, + Label, +} from '@headlessui/react'; +import { ChevronDownIcon } from '@heroicons/react/20/solid'; +import { useState } from 'react'; +import { t } from '@/lib/i18n'; const people = [ - { id: 1, name: 'Leslie Alexander' }, - // More users... -] + { id: 1, name: 'Leslie Alexander' }, + // More users... +]; export default function Example() { - const [query, setQuery] = useState('') - const [selectedPerson, setSelectedPerson] = useState(null) + const [query, setQuery] = useState(''); + const [selectedPerson, setSelectedPerson] = useState(null); - const filteredPeople = - query === '' - ? people - : people.filter((person) => { - return person.name.toLowerCase().includes(query.toLowerCase()) - }) + const filteredPeople = + query === '' + ? people + : people.filter((person) => { + return person.name + .toLowerCase() + .includes(query.toLowerCase()); + }); - return ( - { - setQuery('') - setSelectedPerson(person) - }} - > - -
- setQuery(event.target.value)} - onBlur={() => setQuery('')} - displayValue={(person) => person?.name} - /> - - - - { + setQuery(''); + setSelectedPerson(person); + }} > - {query.length > 0 && ( - - {query} - - )} - {filteredPeople.map((person) => ( - - {person.name} - - ))} - -
-
- ) + +
+ setQuery(event.target.value)} + onBlur={() => setQuery('')} + displayValue={(person) => person?.name} + /> + + + + + {query.length > 0 && ( + + {query} + + )} + {filteredPeople.map((person) => ( + + + {person.name} + + + ))} + +
+ + ); } diff --git a/resources/js/components/examples/bento.tsx b/resources/js/components/examples/bento.tsx index 1514d3e..3729a86 100644 --- a/resources/js/components/examples/bento.tsx +++ b/resources/js/components/examples/bento.tsx @@ -1,102 +1,119 @@ +import { t } from '@/lib/i18n'; + export default function Example() { - return ( -
-
-
-
-
- - -
-

Releases

-

Push to deploy

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. In gravida justo et nulla efficitur, maximus - egestas sem pellentesque. -

-
+ return ( +
+
+
+
+
+ + +
+

+ {t('layout.examples.bento.releases')} +

+

+ {t('layout.examples.bento.push_to_deploy')} +

+

+ Lorem ipsum dolor sit amet, consectetur + adipiscing elit. In gravida justo et nulla + efficitur, maximus egestas sem pellentesque. +

+
+
+
+
+
+ + +
+

+ {t('layout.examples.bento.integrations')} +

+

+ Connect your favorite tools +

+

+ Curabitur auctor, ex quis auctor venenatis, + eros arcu rhoncus massa. +

+
+
+
+
+
+ + +
+

+ {t('layout.examples.bento.security')} +

+

+ Advanced access control +

+

+ Vestibulum ante ipsum primis in faucibus + orci luctus et ultrices posuere cubilia. +

+
+
+
+
+
+ + +
+

+ {t('layout.examples.bento.performance')} +

+

+ Lightning-fast builds +

+

+ Sed congue eros non finibus molestie. + Vestibulum euismod augue vel commodo + vulputate. Maecenas at augue sed elit dictum + vulputate. +

+
+
+
+
-
-
-
- - -
-

Integrations

-

- Connect your favorite tools -

-

- Curabitur auctor, ex quis auctor venenatis, eros arcu rhoncus massa. -

-
-
-
-
-
- - -
-

Security

-

- Advanced access control -

-

- Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia. -

-
-
-
-
-
- - -
-

Performance

-

- Lightning-fast builds -

-

- Sed congue eros non finibus molestie. Vestibulum euismod augue vel commodo vulputate. Maecenas at - augue sed elit dictum vulputate. -

-
-
-
-
-
- ) + ); } diff --git a/resources/js/components/examples/comments.tsx b/resources/js/components/examples/comments.tsx index 5742709..4f57647 100644 --- a/resources/js/components/examples/comments.tsx +++ b/resources/js/components/examples/comments.tsx @@ -1,155 +1,227 @@ -import { Fragment } from 'react' -import { ChatBubbleLeftEllipsisIcon, TagIcon, UserCircleIcon } from '@heroicons/react/20/solid' +import { + ChatBubbleLeftEllipsisIcon, + TagIcon, + UserCircleIcon, +} from '@heroicons/react/20/solid'; +import { Fragment } from 'react'; const activity = [ - { - id: 1, - type: 'comment', - person: { name: 'Eduardo Benz', href: '#' }, - imageUrl: - 'https://images.unsplash.com/photo-1520785643438-5bf77931f493?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80', - comment: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tincidunt nunc ipsum tempor purus vitae id. Morbi in vestibulum nec varius. Et diam cursus quis sed purus nam.', - date: '6d ago', - }, - { - id: 2, - type: 'assignment', - person: { name: 'Hilary Mahy', href: '#' }, - assigned: { name: 'Kristin Watson', href: '#' }, - date: '2d ago', - }, - { - id: 3, - type: 'tags', - person: { name: 'Hilary Mahy', href: '#' }, - tags: [ - { name: 'Bug', href: '#', color: 'fill-red-500' }, - { name: 'Accessibility', href: '#', color: 'fill-indigo-500' }, - ], - date: '6h ago', - }, - { - id: 4, - type: 'comment', - person: { name: 'Jason Meyers', href: '#' }, - imageUrl: - 'https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80', - comment: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tincidunt nunc ipsum tempor purus vitae id. Morbi in vestibulum nec varius. Et diam cursus quis sed purus nam. Scelerisque amet elit non sit ut tincidunt condimentum. Nisl ultrices eu venenatis diam.', - date: '2h ago', - }, -] + { + id: 1, + type: 'comment', + person: { name: 'Eduardo Benz', href: '#' }, + imageUrl: + 'https://images.unsplash.com/photo-1520785643438-5bf77931f493?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80', + comment: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tincidunt nunc ipsum tempor purus vitae id. Morbi in vestibulum nec varius. Et diam cursus quis sed purus nam.', + date: '6d ago', + }, + { + id: 2, + type: 'assignment', + person: { name: 'Hilary Mahy', href: '#' }, + assigned: { name: 'Kristin Watson', href: '#' }, + date: '2d ago', + }, + { + id: 3, + type: 'tags', + person: { name: 'Hilary Mahy', href: '#' }, + tags: [ + { name: 'Bug', href: '#', color: 'fill-red-500' }, + { name: 'Accessibility', href: '#', color: 'fill-indigo-500' }, + ], + date: '6h ago', + }, + { + id: 4, + type: 'comment', + person: { name: 'Jason Meyers', href: '#' }, + imageUrl: + 'https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80', + comment: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tincidunt nunc ipsum tempor purus vitae id. Morbi in vestibulum nec varius. Et diam cursus quis sed purus nam. Scelerisque amet elit non sit ut tincidunt condimentum. Nisl ultrices eu venenatis diam.', + date: '2h ago', + }, +]; function classNames(...classes) { - return classes.filter(Boolean).join(' ') + return classes.filter(Boolean).join(' '); } export default function Example() { - return ( -
- -
- ) + + ))} + +
+ ); } diff --git a/resources/js/components/examples/grid-list.tsx b/resources/js/components/examples/grid-list.tsx index 4effb61..0f635ed 100644 --- a/resources/js/components/examples/grid-list.tsx +++ b/resources/js/components/examples/grid-list.tsx @@ -1,110 +1,123 @@ -import { EnvelopeIcon, PhoneIcon } from '@heroicons/react/20/solid' +import { EnvelopeIcon, PhoneIcon } from '@heroicons/react/20/solid'; const people = [ - { - name: 'Jane Cooper', - title: 'Regional Paradigm Technician', - role: 'Admin', - email: 'janecooper@example.com', - telephone: '+1-202-555-0170', - imageUrl: - 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', - }, - { - name: 'Cody Fisher', - title: 'Product Directives Officer', - role: 'Admin', - email: 'codyfisher@example.com', - telephone: '+1-202-555-0114', - imageUrl: - 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', - }, - { - name: 'Esther Howard', - title: 'Forward Response Developer', - email: 'estherhoward@example.com', - telephone: '+1-202-555-0143', - role: 'Admin', - imageUrl: - 'https://images.unsplash.com/photo-1520813792240-56fc4a3765a7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', - }, - { - name: 'Jenny Wilson', - title: 'Central Security Manager', - role: 'Admin', - email: 'jennywilson@example.com', - telephone: '+1-202-555-0184', - imageUrl: - 'https://images.unsplash.com/photo-1498551172505-8ee7ad69f235?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', - }, - { - name: 'Kristin Watson', - title: 'Lead Implementation Liaison', - role: 'Admin', - email: 'kristinwatson@example.com', - telephone: '+1-202-555-0191', - imageUrl: - 'https://images.unsplash.com/photo-1532417344469-368f9ae6d187?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', - }, - { - name: 'Cameron Williamson', - title: 'Internal Applications Engineer', - role: 'Admin', - email: 'cameronwilliamson@example.com', - telephone: '+1-202-555-0108', - imageUrl: - 'https://images.unsplash.com/photo-1566492031773-4f4e44671857?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', - }, -] + { + name: 'Jane Cooper', + title: 'Regional Paradigm Technician', + role: 'Admin', + email: 'janecooper@example.com', + telephone: '+1-202-555-0170', + imageUrl: + 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', + }, + { + name: 'Cody Fisher', + title: 'Product Directives Officer', + role: 'Admin', + email: 'codyfisher@example.com', + telephone: '+1-202-555-0114', + imageUrl: + 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', + }, + { + name: 'Esther Howard', + title: 'Forward Response Developer', + email: 'estherhoward@example.com', + telephone: '+1-202-555-0143', + role: 'Admin', + imageUrl: + 'https://images.unsplash.com/photo-1520813792240-56fc4a3765a7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', + }, + { + name: 'Jenny Wilson', + title: 'Central Security Manager', + role: 'Admin', + email: 'jennywilson@example.com', + telephone: '+1-202-555-0184', + imageUrl: + 'https://images.unsplash.com/photo-1498551172505-8ee7ad69f235?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', + }, + { + name: 'Kristin Watson', + title: 'Lead Implementation Liaison', + role: 'Admin', + email: 'kristinwatson@example.com', + telephone: '+1-202-555-0191', + imageUrl: + 'https://images.unsplash.com/photo-1532417344469-368f9ae6d187?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', + }, + { + name: 'Cameron Williamson', + title: 'Internal Applications Engineer', + role: 'Admin', + email: 'cameronwilliamson@example.com', + telephone: '+1-202-555-0108', + imageUrl: + 'https://images.unsplash.com/photo-1566492031773-4f4e44671857?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', + }, +]; export default function Example() { - return ( -
    - {people.map((person) => ( -
  • -
    -
    -
    -

    {person.name}

    - - {person.role} - -
    -

    {person.title}

    -
    - -
    -
    -
    -
  • - -
- -
-
- - ))} - - ) +
+
+
+

+ {person.name} +

+ + {person.role} + +
+

+ {person.title} +

+
+ +
+
+ +
+ + ))} + + ); } diff --git a/resources/js/components/examples/our-team.tsx b/resources/js/components/examples/our-team.tsx index 877f340..9c27766 100644 --- a/resources/js/components/examples/our-team.tsx +++ b/resources/js/components/examples/our-team.tsx @@ -1,128 +1,131 @@ const people = [ - { - name: 'Michael Foster', - role: 'Co-Founder / CTO', - imageUrl: - 'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Dries Vincent', - role: 'Business Relations', - imageUrl: - 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Lindsay Walton', - role: 'Front-end Developer', - imageUrl: - 'https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Courtney Henry', - role: 'Designer', - imageUrl: - 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Tom Cook', - role: 'Director of Product', - imageUrl: - 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Whitney Francis', - role: 'Copywriter', - imageUrl: - 'https://images.unsplash.com/photo-1517365830460-955ce3ccd263?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Leonard Krasner', - role: 'Senior Designer', - imageUrl: - 'https://images.unsplash.com/photo-1519345182560-3f2917c472ef?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Floyd Miles', - role: 'Principal Designer', - imageUrl: - 'https://images.unsplash.com/photo-1463453091185-61582044d556?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Emily Selman', - role: 'VP, User Experience', - imageUrl: - 'https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Kristin Watson', - role: 'VP, Human Resources', - imageUrl: - 'https://images.unsplash.com/photo-1500917293891-ef795e70e1f6?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Emma Dorsey', - role: 'Senior Developer', - imageUrl: - 'https://images.unsplash.com/photo-1505840717430-882ce147ef2d?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Alicia Bell', - role: 'Junior Copywriter', - imageUrl: - 'https://images.unsplash.com/photo-1509783236416-c9ad59bae472?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Jenny Wilson', - role: 'Studio Artist', - imageUrl: - 'https://images.unsplash.com/photo-1507101105822-7472b28e22ac?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Anna Roberts', - role: 'Partner, Creative', - imageUrl: - 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, - { - name: 'Benjamin Russel', - role: 'Director, Print Operations', - imageUrl: - 'https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - }, -] + { + name: 'Michael Foster', + role: 'Co-Founder / CTO', + imageUrl: + 'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Dries Vincent', + role: 'Business Relations', + imageUrl: + 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Lindsay Walton', + role: 'Front-end Developer', + imageUrl: + 'https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Courtney Henry', + role: 'Designer', + imageUrl: + 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Tom Cook', + role: 'Director of Product', + imageUrl: + 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Whitney Francis', + role: 'Copywriter', + imageUrl: + 'https://images.unsplash.com/photo-1517365830460-955ce3ccd263?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Leonard Krasner', + role: 'Senior Designer', + imageUrl: + 'https://images.unsplash.com/photo-1519345182560-3f2917c472ef?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Floyd Miles', + role: 'Principal Designer', + imageUrl: + 'https://images.unsplash.com/photo-1463453091185-61582044d556?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Emily Selman', + role: 'VP, User Experience', + imageUrl: + 'https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Kristin Watson', + role: 'VP, Human Resources', + imageUrl: + 'https://images.unsplash.com/photo-1500917293891-ef795e70e1f6?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Emma Dorsey', + role: 'Senior Developer', + imageUrl: + 'https://images.unsplash.com/photo-1505840717430-882ce147ef2d?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Alicia Bell', + role: 'Junior Copywriter', + imageUrl: + 'https://images.unsplash.com/photo-1509783236416-c9ad59bae472?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Jenny Wilson', + role: 'Studio Artist', + imageUrl: + 'https://images.unsplash.com/photo-1507101105822-7472b28e22ac?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Anna Roberts', + role: 'Partner, Creative', + imageUrl: + 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, + { + name: 'Benjamin Russel', + role: 'Director, Print Operations', + imageUrl: + 'https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + }, +]; export default function Example() { - return ( -
-
-
-

- Our team -

-

- We’re a dynamic group of individuals who are passionate about what we do and dedicated to delivering the - best results for our clients. -

+ return ( +
+
+
+

+ Our team +

+

+ We’re a dynamic group of individuals who are passionate + about what we do and dedicated to delivering the best + results for our clients. +

+
+
    + {people.map((person) => ( +
  • + +

    + {person.name} +

    +

    + {person.role} +

    +
  • + ))} +
+
-
    - {people.map((person) => ( -
  • - -

    - {person.name} -

    -

    {person.role}

    -
  • - ))} -
-
-
- ) + ); } diff --git a/resources/js/components/examples/profile-heading.tsx b/resources/js/components/examples/profile-heading.tsx index b2b41aa..0d53629 100644 --- a/resources/js/components/examples/profile-heading.tsx +++ b/resources/js/components/examples/profile-heading.tsx @@ -1,63 +1,83 @@ -import { EnvelopeIcon, PhoneIcon } from '@heroicons/react/20/solid' +import { EnvelopeIcon, PhoneIcon } from '@heroicons/react/20/solid'; +import { t } from '@/lib/i18n'; const profile = { - name: 'Ricardo Cooper', - email: 'ricardo.cooper@example.com', - avatar: - 'https://images.unsplash.com/photo-1463453091185-61582044d556?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', - backgroundImage: - 'https://images.unsplash.com/photo-1444628838545-ac4016a5418a?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80', - fields: [ - ['Phone', '(555) 123-4567'], - ['Email', 'ricardocooper@example.com'], - ['Title', 'Senior Front-End Developer'], - ['Team', 'Product Development'], - ['Location', 'San Francisco'], - ['Sits', 'Oasis, 4th floor'], - ['Salary', '$145,000'], - ['Birthday', 'June 8, 1990'], - ], -} + name: 'Ricardo Cooper', + email: 'ricardo.cooper@example.com', + avatar: 'https://images.unsplash.com/photo-1463453091185-61582044d556?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=1024&h=1024&q=80', + backgroundImage: + 'https://images.unsplash.com/photo-1444628838545-ac4016a5418a?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80', + fields: [ + ['Phone', '(555) 123-4567'], + ['Email', 'ricardocooper@example.com'], + ['Title', 'Senior Front-End Developer'], + ['Team', 'Product Development'], + ['Location', 'San Francisco'], + ['Sits', 'Oasis, 4th floor'], + ['Salary', '$145,000'], + ['Birthday', 'June 8, 1990'], + ], +}; export default function Example() { - return ( -
- -
-
-
+ return ( +
-
-
-
-

{profile.name}

-
-
- - +
+
+
+ +
+
+
+

+ {profile.name} +

+
+
+ + +
+
+
+
+

+ {profile.name} +

+
-
-
-
-

{profile.name}

-
-
- ) + ); } diff --git a/resources/js/components/examples/project-history.tsx b/resources/js/components/examples/project-history.tsx index 5742709..4f57647 100644 --- a/resources/js/components/examples/project-history.tsx +++ b/resources/js/components/examples/project-history.tsx @@ -1,155 +1,227 @@ -import { Fragment } from 'react' -import { ChatBubbleLeftEllipsisIcon, TagIcon, UserCircleIcon } from '@heroicons/react/20/solid' +import { + ChatBubbleLeftEllipsisIcon, + TagIcon, + UserCircleIcon, +} from '@heroicons/react/20/solid'; +import { Fragment } from 'react'; const activity = [ - { - id: 1, - type: 'comment', - person: { name: 'Eduardo Benz', href: '#' }, - imageUrl: - 'https://images.unsplash.com/photo-1520785643438-5bf77931f493?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80', - comment: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tincidunt nunc ipsum tempor purus vitae id. Morbi in vestibulum nec varius. Et diam cursus quis sed purus nam.', - date: '6d ago', - }, - { - id: 2, - type: 'assignment', - person: { name: 'Hilary Mahy', href: '#' }, - assigned: { name: 'Kristin Watson', href: '#' }, - date: '2d ago', - }, - { - id: 3, - type: 'tags', - person: { name: 'Hilary Mahy', href: '#' }, - tags: [ - { name: 'Bug', href: '#', color: 'fill-red-500' }, - { name: 'Accessibility', href: '#', color: 'fill-indigo-500' }, - ], - date: '6h ago', - }, - { - id: 4, - type: 'comment', - person: { name: 'Jason Meyers', href: '#' }, - imageUrl: - 'https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80', - comment: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tincidunt nunc ipsum tempor purus vitae id. Morbi in vestibulum nec varius. Et diam cursus quis sed purus nam. Scelerisque amet elit non sit ut tincidunt condimentum. Nisl ultrices eu venenatis diam.', - date: '2h ago', - }, -] + { + id: 1, + type: 'comment', + person: { name: 'Eduardo Benz', href: '#' }, + imageUrl: + 'https://images.unsplash.com/photo-1520785643438-5bf77931f493?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80', + comment: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tincidunt nunc ipsum tempor purus vitae id. Morbi in vestibulum nec varius. Et diam cursus quis sed purus nam.', + date: '6d ago', + }, + { + id: 2, + type: 'assignment', + person: { name: 'Hilary Mahy', href: '#' }, + assigned: { name: 'Kristin Watson', href: '#' }, + date: '2d ago', + }, + { + id: 3, + type: 'tags', + person: { name: 'Hilary Mahy', href: '#' }, + tags: [ + { name: 'Bug', href: '#', color: 'fill-red-500' }, + { name: 'Accessibility', href: '#', color: 'fill-indigo-500' }, + ], + date: '6h ago', + }, + { + id: 4, + type: 'comment', + person: { name: 'Jason Meyers', href: '#' }, + imageUrl: + 'https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80', + comment: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tincidunt nunc ipsum tempor purus vitae id. Morbi in vestibulum nec varius. Et diam cursus quis sed purus nam. Scelerisque amet elit non sit ut tincidunt condimentum. Nisl ultrices eu venenatis diam.', + date: '2h ago', + }, +]; function classNames(...classes) { - return classes.filter(Boolean).join(' ') + return classes.filter(Boolean).join(' '); } export default function Example() { - return ( -
- -
- ) + + ))} + +
+ ); } diff --git a/resources/js/components/nav-main.tsx b/resources/js/components/nav-main.tsx index 54095ac..e7be1d4 100644 --- a/resources/js/components/nav-main.tsx +++ b/resources/js/components/nav-main.tsx @@ -7,6 +7,7 @@ import { SidebarMenuItem, } from '@/components/ui/sidebar'; import { useCurrentUrl } from '@/hooks/use-current-url'; +import { t } from '@/lib/i18n'; import type { NavItem } from '@/types'; export function NavMain({ items = [] }: { items: NavItem[] }) { @@ -14,7 +15,7 @@ export function NavMain({ items = [] }: { items: NavItem[] }) { return ( - Platform + {t('layout.nav.platform')} {items.map((item) => ( diff --git a/resources/js/components/two-factor-recovery-codes.tsx b/resources/js/components/two-factor-recovery-codes.tsx index a5ee839..b64f26d 100644 --- a/resources/js/components/two-factor-recovery-codes.tsx +++ b/resources/js/components/two-factor-recovery-codes.tsx @@ -9,8 +9,9 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; -import AlertError from './alert-error'; +import { t } from '@/lib/i18n'; import { regenerateRecoveryCodes } from '@/routes/two-factor'; +import AlertError from './alert-error'; type Props = { recoveryCodesList: string[]; @@ -57,11 +58,10 @@ export default function TwoFactorRecoveryCodes({ - Recovery codes let you regain access if you lose your 2FA - device. Store them in a secure password manager. + {t('auth.ui.two_factor_recovery_codes.description')} @@ -76,7 +76,10 @@ export default function TwoFactorRecoveryCodes({ className="size-4" aria-hidden="true" /> - {codesAreVisible ? 'Hide' : 'View'} Recovery Codes + {codesAreVisible + ? t('auth.ui.two_factor_recovery_codes.hide') + : t('auth.ui.two_factor_recovery_codes.view')}{' '} + {t('auth.ui.two_factor_recovery_codes.codes_button_suffix')} {canRegenerateCodes && ( @@ -92,7 +95,7 @@ export default function TwoFactorRecoveryCodes({ disabled={processing} aria-describedby="regenerate-warning" > - Regenerate Codes + {t('auth.ui.two_factor_recovery_codes.regenerate')} )} @@ -112,7 +115,9 @@ export default function TwoFactorRecoveryCodes({ ref={codesSectionRef} className="grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm" role="list" - aria-label="Recovery codes" + aria-label={t( + 'auth.ui.two_factor_recovery_codes.a11y.recovery_codes', + )} > {recoveryCodesList.length ? ( recoveryCodesList.map((code, index) => ( @@ -127,7 +132,9 @@ export default function TwoFactorRecoveryCodes({ ) : (
{Array.from( { length: 8 }, @@ -145,13 +152,11 @@ export default function TwoFactorRecoveryCodes({

- Each recovery code can be used once to - access your account and will be removed - after use. If you need more, click{' '} + {t('auth.ui.two_factor_recovery_codes.usage_warning_prefix')}{' '} - Regenerate Codes + {t('auth.ui.two_factor_recovery_codes.regenerate')} {' '} - above. + {t('auth.ui.two_factor_recovery_codes.usage_warning_suffix')}

diff --git a/resources/js/components/two-factor-setup-modal.tsx b/resources/js/components/two-factor-setup-modal.tsx index 3a898fc..6400513 100644 --- a/resources/js/components/two-factor-setup-modal.tsx +++ b/resources/js/components/two-factor-setup-modal.tsx @@ -19,9 +19,10 @@ import { import { useAppearance } from '@/hooks/use-appearance'; import { useClipboard } from '@/hooks/use-clipboard'; import { OTP_MAX_LENGTH } from '@/hooks/use-two-factor-auth'; +import { t } from '@/lib/i18n'; +import { confirm } from '@/routes/two-factor'; import AlertError from './alert-error'; import { Spinner } from './ui/spinner'; -import { confirm } from '@/routes/two-factor'; function GridScanIcon() { return ( @@ -104,7 +105,7 @@ function TwoFactorSetupStep({
- or, enter the code manually + {t('auth.ui.two_factor_setup_modal.manual_code_separator')}
@@ -209,7 +210,7 @@ function TwoFactorVerificationStep({ onClick={onBack} disabled={processing} > - Back + {t('auth.ui.two_factor_setup_modal.back')}
@@ -261,27 +262,24 @@ export default function TwoFactorSetupModal({ }>(() => { if (twoFactorEnabled) { return { - title: 'Two-Factor Authentication Enabled', - description: - 'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.', - buttonText: 'Close', + title: t('auth.ui.two_factor_setup_modal.enabled_title'), + description: t('auth.ui.two_factor_setup_modal.enabled_description'), + buttonText: t('auth.ui.two_factor_setup_modal.close'), }; } if (showVerificationStep) { return { - title: 'Verify Authentication Code', - description: - 'Enter the 6-digit code from your authenticator app', - buttonText: 'Continue', + title: t('auth.ui.two_factor_setup_modal.verify_title'), + description: t('auth.ui.two_factor_setup_modal.verify_description'), + buttonText: t('auth.ui.two_factor_setup_modal.continue'), }; } return { - title: 'Enable Two-Factor Authentication', - description: - 'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app', - buttonText: 'Continue', + title: t('auth.ui.two_factor_setup_modal.enable_title'), + description: t('auth.ui.two_factor_setup_modal.enable_description'), + buttonText: t('auth.ui.two_factor_setup_modal.continue'), }; }, [twoFactorEnabled, showVerificationStep]); diff --git a/resources/js/components/ui/CoverPhoto.tsx b/resources/js/components/ui/CoverPhoto.tsx index c3d72f1..b519964 100644 --- a/resources/js/components/ui/CoverPhoto.tsx +++ b/resources/js/components/ui/CoverPhoto.tsx @@ -2,6 +2,7 @@ import { router } from '@inertiajs/react' import { useCallback, useRef, useState } from 'react' import { toast } from 'sonner' import { uploadCoverPhoto, updateCoverPhotoPosition } from '@/actions/App/Http/Controllers/UserProfileController' +import { t } from '@/lib/i18n' interface CoverPhotoProps { imageUrl: string | null @@ -117,7 +118,7 @@ export default function CoverPhoto({ imageUrl, baseUrl, positionY, isOwner }: Co }} className="rounded-full bg-black/50 px-3 py-1.5 text-xs font-medium text-white backdrop-blur-sm hover:bg-black/70" > - Reposicionar + {t('layout.cover_photo.reposition')} )}
)} @@ -139,7 +140,7 @@ export default function CoverPhoto({ imageUrl, baseUrl, positionY, isOwner }: Co disabled={saving} className="rounded-full bg-white/90 px-3 py-1.5 text-xs font-medium text-gray-900 backdrop-blur-sm hover:bg-white disabled:opacity-50" > - Cancelar + {t('layout.cover_photo.cancel')}
)} {isOwner && repositioning && (
- Arrastra para reposicionar + {t('layout.cover_photo.drag_hint')}
)} diff --git a/resources/js/components/ui/LoginModal.tsx b/resources/js/components/ui/LoginModal.tsx index 7754cf4..3b0bb34 100644 --- a/resources/js/components/ui/LoginModal.tsx +++ b/resources/js/components/ui/LoginModal.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from 'react' import { Form, router } from '@inertiajs/react' import { ArrowLeftIcon } from 'lucide-react' +import { useEffect, useState } from 'react' import InputError from '@/components/input-error' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' @@ -8,6 +8,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Spinner } from '@/components/ui/spinner' +import { t } from '@/lib/i18n' import { redirect as googleRedirect } from '@/routes/auth/google' import { store as loginStore } from '@/routes/login' import { store as registerStore } from '@/routes/register' @@ -67,9 +68,9 @@ export default function LoginModal() { {view === 'choose' && ( <> - Inicia sesión + {t('auth.ui.login_modal.title')} - Necesitas iniciar sesión para realizar esta acción. + {t('auth.ui.login_modal.description')}
@@ -81,7 +82,7 @@ export default function LoginModal() { - Continuar con correo y contraseña + {t('auth.ui.login_modal.continue_email')}
@@ -110,10 +111,10 @@ export default function LoginModal() { > - Iniciar sesión + {t('auth.ui.login_modal.log_in')} - Ingresa tu correo y contraseña para continuar. + {t('auth.ui.login_modal.credentials_description')}
( <>
- +
- +
- +

- ¿No tienes cuenta?{' '} + {t('auth.ui.login_modal.no_account_prompt')}{' '}

@@ -183,10 +184,10 @@ export default function LoginModal() { > - Crear cuenta + {t('auth.ui.login_modal.register_title')} - Completa los datos para registrarte. + {t('auth.ui.login_modal.register_description')} ( <>
- +
- +
- +
- +

- ¿Ya tienes cuenta?{' '} + {t('auth.ui.login_modal.have_account_prompt')}{' '}

diff --git a/resources/js/components/ui/MobileSidebar.tsx b/resources/js/components/ui/MobileSidebar.tsx index 84cf620..a00e1b5 100644 --- a/resources/js/components/ui/MobileSidebar.tsx +++ b/resources/js/components/ui/MobileSidebar.tsx @@ -1,7 +1,8 @@ -import { useEffect, useRef } from 'react'; -import { ElDialog, ElDialogBackdrop, ElDialogPanel } from '@tailwindplus/elements/react'; import { XMarkIcon } from '@heroicons/react/24/outline'; +import { ElDialog, ElDialogBackdrop, ElDialogPanel } from '@tailwindplus/elements/react'; +import { useEffect, useRef } from 'react'; import WelcomeSidebar from '@/components/ui/WelcomeSidebar'; +import { t } from '@/lib/i18n'; type ShowHideElement = HTMLElement & { show(): void; hide(): void }; @@ -51,7 +52,7 @@ export default function MobileSidebar({ open, onClose, currentPage }: MobileSide
diff --git a/resources/js/components/ui/WelcomeSidebar.tsx b/resources/js/components/ui/WelcomeSidebar.tsx index c1e322e..0895c14 100644 --- a/resources/js/components/ui/WelcomeSidebar.tsx +++ b/resources/js/components/ui/WelcomeSidebar.tsx @@ -1,4 +1,3 @@ -import { Link, usePage } from '@inertiajs/react' import { HomeIcon, BellIcon, @@ -9,6 +8,8 @@ import { MagnifyingGlassIcon, } from '@heroicons/react/24/outline' import { PlusIcon } from '@heroicons/react/24/outline' +import { Link, usePage } from '@inertiajs/react' +import { t } from '@/lib/i18n' import { home, configuracion } from '@/routes/index' import type { Auth } from '@/types/auth' @@ -33,13 +34,13 @@ export default function WelcomeSidebar({ currentPage = 'home' }: WelcomeSidebarP const user = auth?.user const navigation = [ - { id: 'home' as CurrentPage, name: 'Inicio', href: home(), icon: HomeIcon }, - { id: null, name: 'Explorar', href: '#', icon: MagnifyingGlassIcon }, - { id: null, name: 'Notificaciones', href: '#', icon: BellIcon }, - { id: null, name: 'Mensajes', href: '#', icon: EnvelopeIcon }, - { id: null, name: 'Guardados', href: '#', icon: BookmarkIcon }, - { id: null, name: 'Perfil', href: '#', icon: UserIcon }, - { id: 'configuracion' as CurrentPage, name: 'Configuración', href: configuracion(), icon: Cog6ToothIcon }, + { id: 'home' as CurrentPage, name: t('layout.welcome_sidebar.home'), href: home(), icon: HomeIcon }, + { id: null, name: t('layout.welcome_sidebar.explore'), href: '#', icon: MagnifyingGlassIcon }, + { id: null, name: t('layout.welcome_sidebar.notifications'), href: '#', icon: BellIcon }, + { id: null, name: t('layout.welcome_sidebar.messages'), href: '#', icon: EnvelopeIcon }, + { id: null, name: t('layout.welcome_sidebar.bookmarks'), href: '#', icon: BookmarkIcon }, + { id: null, name: t('layout.welcome_sidebar.profile'), href: '#', icon: UserIcon }, + { id: 'configuracion' as CurrentPage, name: t('layout.welcome_sidebar.settings'), href: configuracion(), icon: Cog6ToothIcon }, ] return ( @@ -90,7 +91,7 @@ export default function WelcomeSidebar({ currentPage = 'home' }: WelcomeSidebarP

- Comunidades + {t('layout.welcome_sidebar.communities')}

    {communities.map((c) => ( @@ -120,7 +121,7 @@ export default function WelcomeSidebar({ currentPage = 'home' }: WelcomeSidebarP className="flex w-full items-center justify-center gap-x-2 rounded-full bg-indigo-600 px-4 py-3 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > - Nuevo post + {t('layout.welcome_sidebar.new_post')}
@@ -151,7 +152,7 @@ export default function WelcomeSidebar({ currentPage = 'home' }: WelcomeSidebarP href="/auth/login" className="flex items-center gap-x-3 rounded-full px-3 py-3 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5" > - Iniciar sesión + {t('layout.welcome_sidebar.log_in')} )} diff --git a/resources/js/components/ui/breadcrumb.tsx b/resources/js/components/ui/breadcrumb.tsx index 12a631e..9d81288 100644 --- a/resources/js/components/ui/breadcrumb.tsx +++ b/resources/js/components/ui/breadcrumb.tsx @@ -2,10 +2,11 @@ import { Slot } from "@radix-ui/react-slot" import { ChevronRight, MoreHorizontal } from "lucide-react" import * as React from "react" +import { t } from "@/lib/i18n" import { cn } from "@/lib/utils" function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return