Add comprehensive i18n localization support
- Add go-i18n package for internationalization - Create i18n middleware to extract Accept-Language header - Add translation files for en, es, fr, de, pt languages - Localize all handler error messages and responses - Add language context to all API handlers Supported languages: English, Spanish, French, German, Portuguese 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/treytartt/casera-api/internal/config"
|
"github.com/treytartt/casera-api/internal/config"
|
||||||
"github.com/treytartt/casera-api/internal/database"
|
"github.com/treytartt/casera-api/internal/database"
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/push"
|
"github.com/treytartt/casera-api/internal/push"
|
||||||
"github.com/treytartt/casera-api/internal/router"
|
"github.com/treytartt/casera-api/internal/router"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
@@ -31,6 +32,13 @@ func main() {
|
|||||||
// Initialize logger
|
// Initialize logger
|
||||||
utils.InitLogger(cfg.Server.Debug)
|
utils.InitLogger(cfg.Server.Debug)
|
||||||
|
|
||||||
|
// Initialize i18n
|
||||||
|
if err := i18n.Init(); err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to initialize i18n - using English only")
|
||||||
|
} else {
|
||||||
|
log.Info().Strs("languages", i18n.SupportedLanguages).Msg("i18n initialized")
|
||||||
|
}
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Bool("debug", cfg.Server.Debug).
|
Bool("debug", cfg.Server.Debug).
|
||||||
Int("port", cfg.Server.Port).
|
Int("port", cfg.Server.Port).
|
||||||
|
|||||||
412
docs/# Full Localization Plan for Casera.md
Normal file
412
docs/# Full Localization Plan for Casera.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# Full Localization Plan for Casera
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Complete localization of the Casera property management app across three codebases:
|
||||||
|
- **Go API** - Server-side localization of errors, emails, push notifications, lookup data
|
||||||
|
- **KMM/Android** - Compose Multiplatform string resources
|
||||||
|
- **iOS** - Apple String Catalogs (.xcstrings)
|
||||||
|
|
||||||
|
**Target Languages**: English (base), Spanish, French, German, Portuguese (extensible)
|
||||||
|
|
||||||
|
**Key Decisions**:
|
||||||
|
- API returns localized strings (server-side translation)
|
||||||
|
- Accept-Language header determines locale
|
||||||
|
- Clients display what API returns for API content
|
||||||
|
- Clients localize their own UI strings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: Go API Localization
|
||||||
|
|
||||||
|
### 1.1 Add Dependency
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/nicksnyder/go-i18n/v2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
myCribAPI-go/
|
||||||
|
├── internal/
|
||||||
|
│ └── i18n/
|
||||||
|
│ ├── i18n.go # Core setup, bundle, T() helper
|
||||||
|
│ ├── middleware.go # Gin middleware for Accept-Language
|
||||||
|
│ └── translations/
|
||||||
|
│ ├── en.json # English (base)
|
||||||
|
│ ├── es.json # Spanish
|
||||||
|
│ ├── fr.json # French
|
||||||
|
│ ├── de.json # German
|
||||||
|
│ └── pt.json # Portuguese
|
||||||
|
├── templates/
|
||||||
|
│ └── emails/
|
||||||
|
│ ├── en/
|
||||||
|
│ │ ├── welcome.html
|
||||||
|
│ │ ├── verification.html
|
||||||
|
│ │ └── password_reset.html
|
||||||
|
│ ├── es/
|
||||||
|
│ ├── fr/
|
||||||
|
│ ├── de/
|
||||||
|
│ └── pt/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Core Files to Create
|
||||||
|
|
||||||
|
**internal/i18n/i18n.go**:
|
||||||
|
- Initialize i18n bundle with embedded translation files
|
||||||
|
- Provide `T(localizer, messageID, data)` helper function
|
||||||
|
- Load all JSON translation files at startup
|
||||||
|
|
||||||
|
**internal/i18n/middleware.go**:
|
||||||
|
- Parse Accept-Language header
|
||||||
|
- Match against supported languages (en, es, fr, de, pt)
|
||||||
|
- Store localizer in Gin context
|
||||||
|
- Provide `GetLocalizer(c)` helper
|
||||||
|
|
||||||
|
### 1.4 Translation File Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error.invalid_credentials": "Invalid credentials",
|
||||||
|
"error.username_taken": "Username already taken",
|
||||||
|
"error.email_taken": "Email already registered",
|
||||||
|
"error.not_authenticated": "Not authenticated",
|
||||||
|
"error.task_not_found": "Task not found",
|
||||||
|
"error.residence_access_denied": "You don't have access to this property",
|
||||||
|
"message.logged_out": "Logged out successfully",
|
||||||
|
"message.task_deleted": "Task deleted successfully",
|
||||||
|
"push.task_due_soon.title": "Task Due Soon",
|
||||||
|
"push.task_due_soon.body": "{{.TaskTitle}} is due {{.DueDate}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.5 Handler Update Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Before
|
||||||
|
c.JSON(400, gin.H{"error": "Invalid credentials"})
|
||||||
|
|
||||||
|
// After
|
||||||
|
localizer := i18n.GetLocalizer(c)
|
||||||
|
c.JSON(400, gin.H{"error": i18n.T(localizer, "error.invalid_credentials", nil)})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.6 Database for Lookup Translations
|
||||||
|
|
||||||
|
Add translation table for task categories, priorities, statuses, frequencies:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE lookup_translations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
table_name VARCHAR(50) NOT NULL,
|
||||||
|
record_id INT NOT NULL,
|
||||||
|
locale VARCHAR(10) NOT NULL,
|
||||||
|
field_name VARCHAR(50) NOT NULL,
|
||||||
|
translated_value TEXT NOT NULL,
|
||||||
|
UNIQUE(table_name, record_id, locale, field_name)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Update lookup endpoints to return translated names based on locale.
|
||||||
|
|
||||||
|
### 1.7 Critical Go Files to Modify
|
||||||
|
|
||||||
|
| File | Action | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `internal/i18n/i18n.go` | CREATE | Core i18n bundle and helpers |
|
||||||
|
| `internal/i18n/middleware.go` | CREATE | Locale detection middleware |
|
||||||
|
| `internal/i18n/translations/*.json` | CREATE | Translation files (5 languages) |
|
||||||
|
| `internal/router/router.go` | MODIFY | Add i18n middleware |
|
||||||
|
| `internal/handlers/auth_handler.go` | MODIFY | Localize ~22 error strings |
|
||||||
|
| `internal/handlers/task_handler.go` | MODIFY | Localize ~30 error strings |
|
||||||
|
| `internal/handlers/residence_handler.go` | MODIFY | Localize ~20 error strings |
|
||||||
|
| `internal/handlers/contractor_handler.go` | MODIFY | Localize ~15 error strings |
|
||||||
|
| `internal/handlers/document_handler.go` | MODIFY | Localize ~15 error strings |
|
||||||
|
| `internal/services/email_service.go` | MODIFY | Template-based emails |
|
||||||
|
| `internal/services/notification_service.go` | MODIFY | Localized push content |
|
||||||
|
| `internal/services/lookup_service.go` | MODIFY | Return translated lookups |
|
||||||
|
| `templates/emails/**` | CREATE | Email templates per language |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: KMM/Android Localization
|
||||||
|
|
||||||
|
### 2.1 Strategy
|
||||||
|
|
||||||
|
Use Compose Multiplatform Resources (already in build.gradle.kts via `compose.components.resources`).
|
||||||
|
|
||||||
|
### 2.2 Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
MyCribKMM/composeApp/src/commonMain/
|
||||||
|
└── composeResources/
|
||||||
|
├── values/
|
||||||
|
│ └── strings.xml # English (base)
|
||||||
|
├── values-es/
|
||||||
|
│ └── strings.xml # Spanish
|
||||||
|
├── values-fr/
|
||||||
|
│ └── strings.xml # French
|
||||||
|
├── values-de/
|
||||||
|
│ └── strings.xml # German
|
||||||
|
└── values-pt/
|
||||||
|
└── strings.xml # Portuguese
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 String Resource Format
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Auth -->
|
||||||
|
<string name="auth_login_title">Sign In</string>
|
||||||
|
<string name="auth_login_subtitle">Manage your properties with ease</string>
|
||||||
|
<string name="auth_login_username_label">Username or Email</string>
|
||||||
|
<string name="auth_login_password_label">Password</string>
|
||||||
|
<string name="auth_login_button">Sign In</string>
|
||||||
|
<string name="auth_forgot_password">Forgot Password?</string>
|
||||||
|
|
||||||
|
<!-- Properties -->
|
||||||
|
<string name="properties_title">My Properties</string>
|
||||||
|
<string name="properties_empty_title">No properties yet</string>
|
||||||
|
<string name="properties_empty_subtitle">Add your first property to get started!</string>
|
||||||
|
<string name="properties_add_button">Add Property</string>
|
||||||
|
|
||||||
|
<!-- Tasks -->
|
||||||
|
<string name="tasks_title">Tasks</string>
|
||||||
|
<string name="tasks_add_title">Add New Task</string>
|
||||||
|
<string name="tasks_column_overdue">Overdue</string>
|
||||||
|
<string name="tasks_column_due_soon">Due Soon</string>
|
||||||
|
|
||||||
|
<!-- Common -->
|
||||||
|
<string name="common_save">Save</string>
|
||||||
|
<string name="common_cancel">Cancel</string>
|
||||||
|
<string name="common_delete">Delete</string>
|
||||||
|
<string name="common_loading">Loading…</string>
|
||||||
|
<string name="common_error_generic">Something went wrong. Please try again.</string>
|
||||||
|
|
||||||
|
<!-- Accessibility -->
|
||||||
|
<string name="a11y_back">Back</string>
|
||||||
|
<string name="a11y_close">Close</string>
|
||||||
|
<string name="a11y_add_property">Add Property</string>
|
||||||
|
</resources>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Usage in Compose
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import casera.composeapp.generated.resources.Res
|
||||||
|
import casera.composeapp.generated.resources.*
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen() {
|
||||||
|
Text(stringResource(Res.string.auth_login_title))
|
||||||
|
Button(onClick = { /* ... */ }) {
|
||||||
|
Text(stringResource(Res.string.auth_login_button))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 Critical KMM Files to Modify
|
||||||
|
|
||||||
|
| File | Action | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `composeResources/values/strings.xml` | CREATE | Base English strings (~500) |
|
||||||
|
| `composeResources/values-es/strings.xml` | CREATE | Spanish translations |
|
||||||
|
| `composeResources/values-fr/strings.xml` | CREATE | French translations |
|
||||||
|
| `composeResources/values-de/strings.xml` | CREATE | German translations |
|
||||||
|
| `composeResources/values-pt/strings.xml` | CREATE | Portuguese translations |
|
||||||
|
| `ui/screens/LoginScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||||
|
| `ui/screens/ResidencesScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||||
|
| `ui/screens/TasksScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||||
|
| `ui/screens/ContractorsScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||||
|
| `ui/screens/DocumentsScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||||
|
| `ui/components/*.kt` | MODIFY | Replace hardcoded strings (33 files) |
|
||||||
|
| All other screen files | MODIFY | Replace hardcoded strings |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: iOS Localization
|
||||||
|
|
||||||
|
### 3.1 Strategy
|
||||||
|
|
||||||
|
Use Apple String Catalogs (`.xcstrings`) - modern approach with Xcode visual editor.
|
||||||
|
|
||||||
|
### 3.2 Create String Catalog
|
||||||
|
|
||||||
|
1. Xcode: File > New > File > String Catalog
|
||||||
|
2. Name: `Localizable.xcstrings`
|
||||||
|
3. Add languages: English, Spanish, French, German, Portuguese
|
||||||
|
|
||||||
|
### 3.3 Type-Safe String Access
|
||||||
|
|
||||||
|
Create `iosApp/iosApp/Helpers/L10n.swift`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum L10n {
|
||||||
|
enum Auth {
|
||||||
|
static let loginTitle = String(localized: "auth_login_title")
|
||||||
|
static let loginSubtitle = String(localized: "auth_login_subtitle")
|
||||||
|
static let loginUsernameLabel = String(localized: "auth_login_username_label")
|
||||||
|
static let loginPasswordLabel = String(localized: "auth_login_password_label")
|
||||||
|
static let loginButton = String(localized: "auth_login_button")
|
||||||
|
static let forgotPassword = String(localized: "auth_forgot_password")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Properties {
|
||||||
|
static let title = String(localized: "properties_title")
|
||||||
|
static let emptyTitle = String(localized: "properties_empty_title")
|
||||||
|
static let emptySubtitle = String(localized: "properties_empty_subtitle")
|
||||||
|
static let addButton = String(localized: "properties_add_button")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Tasks {
|
||||||
|
static let title = String(localized: "tasks_title")
|
||||||
|
static let addTitle = String(localized: "tasks_add_title")
|
||||||
|
static let columnOverdue = String(localized: "tasks_column_overdue")
|
||||||
|
static let columnDueSoon = String(localized: "tasks_column_due_soon")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Common {
|
||||||
|
static let save = String(localized: "common_save")
|
||||||
|
static let cancel = String(localized: "common_cancel")
|
||||||
|
static let delete = String(localized: "common_delete")
|
||||||
|
static let loading = String(localized: "common_loading")
|
||||||
|
static let errorGeneric = String(localized: "common_error_generic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Usage in SwiftUI
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Before
|
||||||
|
Text("My Properties")
|
||||||
|
.navigationTitle("My Properties")
|
||||||
|
|
||||||
|
// After
|
||||||
|
Text(L10n.Properties.title)
|
||||||
|
.navigationTitle(L10n.Properties.title)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Critical iOS Files to Modify
|
||||||
|
|
||||||
|
| File | Action | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `iosApp/Localizable.xcstrings` | CREATE | String catalog with all translations |
|
||||||
|
| `iosApp/Helpers/L10n.swift` | CREATE | Type-safe string access |
|
||||||
|
| `Login/LoginView.swift` | MODIFY | Replace ~7 hardcoded strings |
|
||||||
|
| `Login/LoginViewModel.swift` | MODIFY | Replace ~12 error messages |
|
||||||
|
| `Register/RegisterView.swift` | MODIFY | Replace ~10 hardcoded strings |
|
||||||
|
| `Residence/*.swift` | MODIFY | Replace ~30 hardcoded strings |
|
||||||
|
| `Task/*.swift` | MODIFY | Replace ~50 hardcoded strings |
|
||||||
|
| `Contractor/*.swift` | MODIFY | Replace ~20 hardcoded strings |
|
||||||
|
| `Document/*.swift` | MODIFY | Replace ~15 hardcoded strings |
|
||||||
|
| `Profile/*.swift` | MODIFY | Replace ~20 hardcoded strings |
|
||||||
|
| All other view files | MODIFY | Replace hardcoded strings |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 4: Implementation Order
|
||||||
|
|
||||||
|
### Phase 1: Infrastructure (Do First)
|
||||||
|
|
||||||
|
1. **Go API**:
|
||||||
|
- Add go-i18n dependency
|
||||||
|
- Create `internal/i18n/` package with i18n.go and middleware.go
|
||||||
|
- Create base `en.json` with all extractable strings
|
||||||
|
- Add middleware to router
|
||||||
|
- Test with curl using Accept-Language header
|
||||||
|
|
||||||
|
2. **KMM**:
|
||||||
|
- Create `composeResources/values/strings.xml` with ~50 core strings
|
||||||
|
- Verify Compose resources compile correctly
|
||||||
|
- Update one screen (LoginScreen) as proof of concept
|
||||||
|
|
||||||
|
3. **iOS**:
|
||||||
|
- Create `Localizable.xcstrings` in Xcode
|
||||||
|
- Create `L10n.swift` helper
|
||||||
|
- Add ~50 core strings
|
||||||
|
- Update LoginView as proof of concept
|
||||||
|
|
||||||
|
### Phase 2: API Full Localization
|
||||||
|
|
||||||
|
1. Update all handlers to use localized errors
|
||||||
|
2. Add lookup_translations table and seed data
|
||||||
|
3. Update lookup service to return translated names
|
||||||
|
4. Move email templates to files, create Spanish versions
|
||||||
|
5. Update push notification service for localized content
|
||||||
|
|
||||||
|
### Phase 3: Mobile String Extraction
|
||||||
|
|
||||||
|
**Order by feature (same for KMM and iOS)**:
|
||||||
|
1. Auth screens (Login, Register, Verify, Password Reset, Apple Sign In)
|
||||||
|
2. Property screens (List, Detail, Form, Join, Share, Manage Users)
|
||||||
|
3. Task screens (List, Detail, Form, Complete, Actions, Kanban)
|
||||||
|
4. Contractor screens (List, Detail, Form)
|
||||||
|
5. Document screens (List, Detail, Form, Warranties)
|
||||||
|
6. Profile screens (Profile, Settings, Notifications)
|
||||||
|
7. Common components (Dialogs, Cards, Empty States, Loading)
|
||||||
|
|
||||||
|
### Phase 4: Create Translation Files
|
||||||
|
|
||||||
|
- Create es.json, fr.json, de.json, pt.json for Go API
|
||||||
|
- Create values-es/, values-fr/, values-de/, values-pt/ for KMM
|
||||||
|
- Add all language translations to iOS String Catalog
|
||||||
|
|
||||||
|
### Phase 5: Testing & Polish
|
||||||
|
|
||||||
|
- Test all screens in each language
|
||||||
|
- Verify email rendering in each language
|
||||||
|
- Test push notifications
|
||||||
|
- Verify lookup data translations
|
||||||
|
- Handle edge cases (long strings, RTL future-proofing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## String Naming Convention
|
||||||
|
|
||||||
|
Use consistent keys across all platforms:
|
||||||
|
|
||||||
|
```
|
||||||
|
<category>_<screen/feature>_<element>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
auth_login_title
|
||||||
|
auth_login_button
|
||||||
|
properties_list_empty_title
|
||||||
|
properties_form_field_name
|
||||||
|
tasks_detail_button_complete
|
||||||
|
common_button_save
|
||||||
|
common_button_cancel
|
||||||
|
error_network_timeout
|
||||||
|
a11y_button_back
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estimated String Counts
|
||||||
|
|
||||||
|
| Platform | Approximate Strings |
|
||||||
|
|----------|---------------------|
|
||||||
|
| Go API - Errors | ~100 |
|
||||||
|
| Go API - Email templates | ~50 per language |
|
||||||
|
| Go API - Push notifications | ~20 |
|
||||||
|
| Go API - Lookup data | ~50 |
|
||||||
|
| KMM/Android | ~500 |
|
||||||
|
| iOS | ~500 |
|
||||||
|
| **Total unique strings** | ~700-800 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Translation Workflow
|
||||||
|
|
||||||
|
1. **Extract**: All English strings defined first
|
||||||
|
2. **Export**: JSON (Go), XML (Android), .xcstrings (iOS)
|
||||||
|
3. **Translate**: Use Lokalise, Crowdin, or manual translation
|
||||||
|
4. **Import**: Place translated files in correct locations
|
||||||
|
5. **Test**: Verify in each language
|
||||||
1
go.mod
1
go.mod
@@ -79,6 +79,7 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -151,6 +151,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||||
"github.com/treytartt/casera-api/internal/dto/responses"
|
"github.com/treytartt/casera-api/internal/dto/responses"
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/middleware"
|
"github.com/treytartt/casera-api/internal/middleware"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
)
|
)
|
||||||
@@ -40,7 +41,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
|||||||
var req requests.LoginRequest
|
var req requests.LoginRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||||
Error: "Invalid request body",
|
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||||
Details: map[string]string{
|
Details: map[string]string{
|
||||||
"validation": err.Error(),
|
"validation": err.Error(),
|
||||||
},
|
},
|
||||||
@@ -51,10 +52,10 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
|||||||
response, err := h.authService.Login(&req)
|
response, err := h.authService.Login(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusUnauthorized
|
status := http.StatusUnauthorized
|
||||||
message := "Invalid credentials"
|
message := i18n.LocalizedMessage(c, "error.invalid_credentials")
|
||||||
|
|
||||||
if errors.Is(err, services.ErrUserInactive) {
|
if errors.Is(err, services.ErrUserInactive) {
|
||||||
message = "Account is inactive"
|
message = i18n.LocalizedMessage(c, "error.account_inactive")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Err(err).Str("identifier", req.Username).Msg("Login failed")
|
log.Debug().Err(err).Str("identifier", req.Username).Msg("Login failed")
|
||||||
@@ -70,7 +71,7 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
|||||||
var req requests.RegisterRequest
|
var req requests.RegisterRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||||
Error: "Invalid request body",
|
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||||
Details: map[string]string{
|
Details: map[string]string{
|
||||||
"validation": err.Error(),
|
"validation": err.Error(),
|
||||||
},
|
},
|
||||||
@@ -84,12 +85,12 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
|||||||
message := err.Error()
|
message := err.Error()
|
||||||
|
|
||||||
if errors.Is(err, services.ErrUsernameTaken) {
|
if errors.Is(err, services.ErrUsernameTaken) {
|
||||||
message = "Username already taken"
|
message = i18n.LocalizedMessage(c, "error.username_taken")
|
||||||
} else if errors.Is(err, services.ErrEmailTaken) {
|
} else if errors.Is(err, services.ErrEmailTaken) {
|
||||||
message = "Email already registered"
|
message = i18n.LocalizedMessage(c, "error.email_taken")
|
||||||
} else {
|
} else {
|
||||||
status = http.StatusInternalServerError
|
status = http.StatusInternalServerError
|
||||||
message = "Registration failed"
|
message = i18n.LocalizedMessage(c, "error.registration_failed")
|
||||||
log.Error().Err(err).Msg("Registration failed")
|
log.Error().Err(err).Msg("Registration failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
|||||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||||
token := middleware.GetAuthToken(c)
|
token := middleware.GetAuthToken(c)
|
||||||
if token == "" {
|
if token == "" {
|
||||||
c.JSON(http.StatusUnauthorized, responses.ErrorResponse{Error: "Not authenticated"})
|
c.JSON(http.StatusUnauthorized, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.not_authenticated")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ func (h *AuthHandler) Logout(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, responses.MessageResponse{Message: "Logged out successfully"})
|
c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.logged_out")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentUser handles GET /api/auth/me/
|
// CurrentUser handles GET /api/auth/me/
|
||||||
@@ -142,7 +143,7 @@ func (h *AuthHandler) CurrentUser(c *gin.Context) {
|
|||||||
response, err := h.authService.GetCurrentUser(user.ID)
|
response, err := h.authService.GetCurrentUser(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to get current user")
|
log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to get current user")
|
||||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: "Failed to get user"})
|
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.failed_to_get_user")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ func (h *AuthHandler) UpdateProfile(c *gin.Context) {
|
|||||||
var req requests.UpdateProfileRequest
|
var req requests.UpdateProfileRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||||
Error: "Invalid request body",
|
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||||
Details: map[string]string{
|
Details: map[string]string{
|
||||||
"validation": err.Error(),
|
"validation": err.Error(),
|
||||||
},
|
},
|
||||||
@@ -170,12 +171,12 @@ func (h *AuthHandler) UpdateProfile(c *gin.Context) {
|
|||||||
response, err := h.authService.UpdateProfile(user.ID, &req)
|
response, err := h.authService.UpdateProfile(user.ID, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrEmailTaken) {
|
if errors.Is(err, services.ErrEmailTaken) {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Error: "Email already taken"})
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.email_already_taken")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to update profile")
|
log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to update profile")
|
||||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: "Failed to update profile"})
|
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.failed_to_update_profile")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
|||||||
var req requests.VerifyEmailRequest
|
var req requests.VerifyEmailRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||||
Error: "Invalid request body",
|
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||||
Details: map[string]string{
|
Details: map[string]string{
|
||||||
"validation": err.Error(),
|
"validation": err.Error(),
|
||||||
},
|
},
|
||||||
@@ -206,14 +207,14 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
|||||||
message := err.Error()
|
message := err.Error()
|
||||||
|
|
||||||
if errors.Is(err, services.ErrInvalidCode) {
|
if errors.Is(err, services.ErrInvalidCode) {
|
||||||
message = "Invalid verification code"
|
message = i18n.LocalizedMessage(c, "error.invalid_verification_code")
|
||||||
} else if errors.Is(err, services.ErrCodeExpired) {
|
} else if errors.Is(err, services.ErrCodeExpired) {
|
||||||
message = "Verification code has expired"
|
message = i18n.LocalizedMessage(c, "error.verification_code_expired")
|
||||||
} else if errors.Is(err, services.ErrAlreadyVerified) {
|
} else if errors.Is(err, services.ErrAlreadyVerified) {
|
||||||
message = "Email already verified"
|
message = i18n.LocalizedMessage(c, "error.email_already_verified")
|
||||||
} else {
|
} else {
|
||||||
status = http.StatusInternalServerError
|
status = http.StatusInternalServerError
|
||||||
message = "Verification failed"
|
message = i18n.LocalizedMessage(c, "error.verification_failed")
|
||||||
log.Error().Err(err).Uint("user_id", user.ID).Msg("Email verification failed")
|
log.Error().Err(err).Uint("user_id", user.ID).Msg("Email verification failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +223,7 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, responses.VerifyEmailResponse{
|
c.JSON(http.StatusOK, responses.VerifyEmailResponse{
|
||||||
Message: "Email verified successfully",
|
Message: i18n.LocalizedMessage(c, "message.email_verified"),
|
||||||
Verified: true,
|
Verified: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -237,12 +238,12 @@ func (h *AuthHandler) ResendVerification(c *gin.Context) {
|
|||||||
code, err := h.authService.ResendVerificationCode(user.ID)
|
code, err := h.authService.ResendVerificationCode(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrAlreadyVerified) {
|
if errors.Is(err, services.ErrAlreadyVerified) {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Error: "Email already verified"})
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.email_already_verified")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to resend verification")
|
log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to resend verification")
|
||||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: "Failed to resend verification"})
|
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.failed_to_resend_verification")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +256,7 @@ func (h *AuthHandler) ResendVerification(c *gin.Context) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, responses.MessageResponse{Message: "Verification email sent"})
|
c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.verification_email_sent")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForgotPassword handles POST /api/auth/forgot-password/
|
// ForgotPassword handles POST /api/auth/forgot-password/
|
||||||
@@ -263,7 +264,7 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
|||||||
var req requests.ForgotPasswordRequest
|
var req requests.ForgotPasswordRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||||
Error: "Invalid request body",
|
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||||
Details: map[string]string{
|
Details: map[string]string{
|
||||||
"validation": err.Error(),
|
"validation": err.Error(),
|
||||||
},
|
},
|
||||||
@@ -275,7 +276,7 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrRateLimitExceeded) {
|
if errors.Is(err, services.ErrRateLimitExceeded) {
|
||||||
c.JSON(http.StatusTooManyRequests, responses.ErrorResponse{
|
c.JSON(http.StatusTooManyRequests, responses.ErrorResponse{
|
||||||
Error: "Too many password reset requests. Please try again later.",
|
Error: i18n.LocalizedMessage(c, "error.rate_limit_exceeded"),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -295,7 +296,7 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
|||||||
|
|
||||||
// Always return success to prevent email enumeration
|
// Always return success to prevent email enumeration
|
||||||
c.JSON(http.StatusOK, responses.ForgotPasswordResponse{
|
c.JSON(http.StatusOK, responses.ForgotPasswordResponse{
|
||||||
Message: "If an account with that email exists, a password reset code has been sent.",
|
Message: i18n.LocalizedMessage(c, "message.password_reset_email_sent"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +305,7 @@ func (h *AuthHandler) VerifyResetCode(c *gin.Context) {
|
|||||||
var req requests.VerifyResetCodeRequest
|
var req requests.VerifyResetCodeRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||||
Error: "Invalid request body",
|
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||||
Details: map[string]string{
|
Details: map[string]string{
|
||||||
"validation": err.Error(),
|
"validation": err.Error(),
|
||||||
},
|
},
|
||||||
@@ -315,13 +316,13 @@ func (h *AuthHandler) VerifyResetCode(c *gin.Context) {
|
|||||||
resetToken, err := h.authService.VerifyResetCode(req.Email, req.Code)
|
resetToken, err := h.authService.VerifyResetCode(req.Email, req.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusBadRequest
|
status := http.StatusBadRequest
|
||||||
message := "Invalid verification code"
|
message := i18n.LocalizedMessage(c, "error.invalid_verification_code")
|
||||||
|
|
||||||
if errors.Is(err, services.ErrCodeExpired) {
|
if errors.Is(err, services.ErrCodeExpired) {
|
||||||
message = "Verification code has expired"
|
message = i18n.LocalizedMessage(c, "error.verification_code_expired")
|
||||||
} else if errors.Is(err, services.ErrRateLimitExceeded) {
|
} else if errors.Is(err, services.ErrRateLimitExceeded) {
|
||||||
status = http.StatusTooManyRequests
|
status = http.StatusTooManyRequests
|
||||||
message = "Too many attempts. Please request a new code."
|
message = i18n.LocalizedMessage(c, "error.too_many_attempts")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(status, responses.ErrorResponse{Error: message})
|
c.JSON(status, responses.ErrorResponse{Error: message})
|
||||||
@@ -329,7 +330,7 @@ func (h *AuthHandler) VerifyResetCode(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, responses.VerifyResetCodeResponse{
|
c.JSON(http.StatusOK, responses.VerifyResetCodeResponse{
|
||||||
Message: "Code verified successfully",
|
Message: i18n.LocalizedMessage(c, "message.reset_code_verified"),
|
||||||
ResetToken: resetToken,
|
ResetToken: resetToken,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -339,7 +340,7 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
|||||||
var req requests.ResetPasswordRequest
|
var req requests.ResetPasswordRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||||
Error: "Invalid request body",
|
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||||
Details: map[string]string{
|
Details: map[string]string{
|
||||||
"validation": err.Error(),
|
"validation": err.Error(),
|
||||||
},
|
},
|
||||||
@@ -350,13 +351,13 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
|||||||
err := h.authService.ResetPassword(req.ResetToken, req.NewPassword)
|
err := h.authService.ResetPassword(req.ResetToken, req.NewPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusBadRequest
|
status := http.StatusBadRequest
|
||||||
message := "Invalid or expired reset token"
|
message := i18n.LocalizedMessage(c, "error.invalid_reset_token")
|
||||||
|
|
||||||
if errors.Is(err, services.ErrInvalidResetToken) {
|
if errors.Is(err, services.ErrInvalidResetToken) {
|
||||||
message = "Invalid or expired reset token"
|
message = i18n.LocalizedMessage(c, "error.invalid_reset_token")
|
||||||
} else {
|
} else {
|
||||||
status = http.StatusInternalServerError
|
status = http.StatusInternalServerError
|
||||||
message = "Password reset failed"
|
message = i18n.LocalizedMessage(c, "error.password_reset_failed")
|
||||||
log.Error().Err(err).Msg("Password reset failed")
|
log.Error().Err(err).Msg("Password reset failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +366,7 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, responses.ResetPasswordResponse{
|
c.JSON(http.StatusOK, responses.ResetPasswordResponse{
|
||||||
Message: "Password reset successfully. Please log in with your new password.",
|
Message: i18n.LocalizedMessage(c, "message.password_reset_success"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +375,7 @@ func (h *AuthHandler) AppleSignIn(c *gin.Context) {
|
|||||||
var req requests.AppleSignInRequest
|
var req requests.AppleSignInRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||||
Error: "Invalid request body",
|
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||||
Details: map[string]string{
|
Details: map[string]string{
|
||||||
"validation": err.Error(),
|
"validation": err.Error(),
|
||||||
},
|
},
|
||||||
@@ -385,7 +386,7 @@ func (h *AuthHandler) AppleSignIn(c *gin.Context) {
|
|||||||
if h.appleAuthService == nil {
|
if h.appleAuthService == nil {
|
||||||
log.Error().Msg("Apple auth service not configured")
|
log.Error().Msg("Apple auth service not configured")
|
||||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{
|
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{
|
||||||
Error: "Apple Sign In is not configured",
|
Error: i18n.LocalizedMessage(c, "error.apple_signin_not_configured"),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -393,12 +394,12 @@ func (h *AuthHandler) AppleSignIn(c *gin.Context) {
|
|||||||
response, err := h.authService.AppleSignIn(c.Request.Context(), h.appleAuthService, &req)
|
response, err := h.authService.AppleSignIn(c.Request.Context(), h.appleAuthService, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusUnauthorized
|
status := http.StatusUnauthorized
|
||||||
message := "Apple Sign In failed"
|
message := i18n.LocalizedMessage(c, "error.apple_signin_failed")
|
||||||
|
|
||||||
if errors.Is(err, services.ErrUserInactive) {
|
if errors.Is(err, services.ErrUserInactive) {
|
||||||
message = "Account is inactive"
|
message = i18n.LocalizedMessage(c, "error.account_inactive")
|
||||||
} else if errors.Is(err, services.ErrAppleSignInFailed) {
|
} else if errors.Is(err, services.ErrAppleSignInFailed) {
|
||||||
message = "Invalid Apple identity token"
|
message = i18n.LocalizedMessage(c, "error.invalid_apple_token")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Err(err).Msg("Apple Sign In failed")
|
log.Debug().Err(err).Msg("Apple Sign In failed")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/middleware"
|
"github.com/treytartt/casera-api/internal/middleware"
|
||||||
"github.com/treytartt/casera-api/internal/models"
|
"github.com/treytartt/casera-api/internal/models"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
@@ -39,7 +40,7 @@ func (h *ContractorHandler) GetContractor(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +48,9 @@ func (h *ContractorHandler) GetContractor(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrContractorNotFound):
|
case errors.Is(err, services.ErrContractorNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ func (h *ContractorHandler) CreateContractor(c *gin.Context) {
|
|||||||
response, err := h.contractorService.CreateContractor(&req, user.ID)
|
response, err := h.contractorService.CreateContractor(&req, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
@@ -84,7 +85,7 @@ func (h *ContractorHandler) UpdateContractor(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +99,9 @@ func (h *ContractorHandler) UpdateContractor(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrContractorNotFound):
|
case errors.Is(err, services.ErrContractorNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -114,7 +115,7 @@ func (h *ContractorHandler) DeleteContractor(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,15 +123,15 @@ func (h *ContractorHandler) DeleteContractor(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrContractorNotFound):
|
case errors.Is(err, services.ErrContractorNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Contractor deleted successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.contractor_deleted")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToggleFavorite handles POST /api/contractors/:id/toggle-favorite/
|
// ToggleFavorite handles POST /api/contractors/:id/toggle-favorite/
|
||||||
@@ -138,7 +139,7 @@ func (h *ContractorHandler) ToggleFavorite(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,9 +147,9 @@ func (h *ContractorHandler) ToggleFavorite(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrContractorNotFound):
|
case errors.Is(err, services.ErrContractorNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -162,7 +163,7 @@ func (h *ContractorHandler) GetContractorTasks(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contractor ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +171,9 @@ func (h *ContractorHandler) GetContractorTasks(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrContractorNotFound):
|
case errors.Is(err, services.ErrContractorNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -186,14 +187,14 @@ func (h *ContractorHandler) ListContractorsByResidence(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32)
|
residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := h.contractorService.ListContractorsByResidence(uint(residenceID), user.ID)
|
response, err := h.contractorService.ListContractorsByResidence(uint(residenceID), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/middleware"
|
"github.com/treytartt/casera-api/internal/middleware"
|
||||||
"github.com/treytartt/casera-api/internal/models"
|
"github.com/treytartt/casera-api/internal/models"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
@@ -47,7 +48,7 @@ func (h *DocumentHandler) GetDocument(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +56,9 @@ func (h *DocumentHandler) GetDocument(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrDocumentNotFound):
|
case errors.Is(err, services.ErrDocumentNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -89,19 +90,19 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
|||||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||||
// Parse multipart form
|
// Parse multipart form
|
||||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to parse multipart form: " + err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_parse_form")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse residence_id (required)
|
// Parse residence_id (required)
|
||||||
residenceIDStr := c.PostForm("residence_id")
|
residenceIDStr := c.PostForm("residence_id")
|
||||||
if residenceIDStr == "" {
|
if residenceIDStr == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "residence_id is required"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_id_required")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
residenceID, err := strconv.ParseUint(residenceIDStr, 10, 32)
|
residenceID, err := strconv.ParseUint(residenceIDStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid residence_id"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.ResidenceID = uint(residenceID)
|
req.ResidenceID = uint(residenceID)
|
||||||
@@ -109,7 +110,7 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
|||||||
// Parse title (required)
|
// Parse title (required)
|
||||||
req.Title = c.PostForm("title")
|
req.Title = c.PostForm("title")
|
||||||
if req.Title == "" {
|
if req.Title == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.title_required")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +171,7 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
|||||||
if uploadedFile != nil && h.storageService != nil {
|
if uploadedFile != nil && h.storageService != nil {
|
||||||
result, err := h.storageService.Upload(uploadedFile, "documents")
|
result, err := h.storageService.Upload(uploadedFile, "documents")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to upload file: " + err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_upload_file")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.FileURL = result.URL
|
req.FileURL = result.URL
|
||||||
@@ -190,7 +191,7 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
|||||||
response, err := h.documentService.CreateDocument(&req, user.ID)
|
response, err := h.documentService.CreateDocument(&req, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
@@ -204,7 +205,7 @@ func (h *DocumentHandler) UpdateDocument(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,9 +219,9 @@ func (h *DocumentHandler) UpdateDocument(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrDocumentNotFound):
|
case errors.Is(err, services.ErrDocumentNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -234,7 +235,7 @@ func (h *DocumentHandler) DeleteDocument(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,15 +243,15 @@ func (h *DocumentHandler) DeleteDocument(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrDocumentNotFound):
|
case errors.Is(err, services.ErrDocumentNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Document deleted successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.document_deleted")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActivateDocument handles POST /api/documents/:id/activate/
|
// ActivateDocument handles POST /api/documents/:id/activate/
|
||||||
@@ -258,7 +259,7 @@ func (h *DocumentHandler) ActivateDocument(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,15 +267,15 @@ func (h *DocumentHandler) ActivateDocument(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrDocumentNotFound):
|
case errors.Is(err, services.ErrDocumentNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Document activated", "document": response})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.document_activated"), "document": response})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeactivateDocument handles POST /api/documents/:id/deactivate/
|
// DeactivateDocument handles POST /api/documents/:id/deactivate/
|
||||||
@@ -282,7 +283,7 @@ func (h *DocumentHandler) DeactivateDocument(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,13 +291,13 @@ func (h *DocumentHandler) DeactivateDocument(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrDocumentNotFound):
|
case errors.Is(err, services.ErrDocumentNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Document deactivated", "document": response})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.document_deactivated"), "document": response})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/middleware"
|
"github.com/treytartt/casera-api/internal/middleware"
|
||||||
"github.com/treytartt/casera-api/internal/models"
|
"github.com/treytartt/casera-api/internal/models"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
@@ -70,21 +71,21 @@ func (h *NotificationHandler) MarkAsRead(c *gin.Context) {
|
|||||||
|
|
||||||
notificationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
notificationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notification ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_notification_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.notificationService.MarkAsRead(uint(notificationID), user.ID)
|
err = h.notificationService.MarkAsRead(uint(notificationID), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrNotificationNotFound) {
|
if errors.Is(err, services.ErrNotificationNotFound) {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.notification_not_found")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Notification marked as read"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.notification_marked_read")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkAllAsRead handles POST /api/notifications/mark-all-read/
|
// MarkAllAsRead handles POST /api/notifications/mark-all-read/
|
||||||
@@ -97,7 +98,7 @@ func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "All notifications marked as read"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.all_notifications_marked_read")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPreferences handles GET /api/notifications/preferences/
|
// GetPreferences handles GET /api/notifications/preferences/
|
||||||
@@ -145,7 +146,7 @@ func (h *NotificationHandler) RegisterDevice(c *gin.Context) {
|
|||||||
device, err := h.notificationService.RegisterDevice(user.ID, &req)
|
device, err := h.notificationService.RegisterDevice(user.ID, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrInvalidPlatform) {
|
if errors.Is(err, services.ErrInvalidPlatform) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_platform")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
@@ -174,7 +175,7 @@ func (h *NotificationHandler) DeleteDevice(c *gin.Context) {
|
|||||||
|
|
||||||
deviceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
deviceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid device ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_device_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,12 +187,12 @@ func (h *NotificationHandler) DeleteDevice(c *gin.Context) {
|
|||||||
err = h.notificationService.DeleteDevice(uint(deviceID), platform, user.ID)
|
err = h.notificationService.DeleteDevice(uint(deviceID), platform, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrInvalidPlatform) {
|
if errors.Is(err, services.ErrInvalidPlatform) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_platform")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Device removed"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.device_removed")})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/middleware"
|
"github.com/treytartt/casera-api/internal/middleware"
|
||||||
"github.com/treytartt/casera-api/internal/models"
|
"github.com/treytartt/casera-api/internal/models"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
@@ -61,7 +62,7 @@ func (h *ResidenceHandler) GetResidence(c *gin.Context) {
|
|||||||
|
|
||||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +70,9 @@ func (h *ResidenceHandler) GetResidence(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrResidenceNotFound):
|
case errors.Is(err, services.ErrResidenceNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||||
case errors.Is(err, services.ErrResidenceAccessDenied):
|
case errors.Is(err, services.ErrResidenceAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -94,7 +95,7 @@ func (h *ResidenceHandler) CreateResidence(c *gin.Context) {
|
|||||||
response, err := h.residenceService.CreateResidence(&req, user.ID)
|
response, err := h.residenceService.CreateResidence(&req, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrPropertiesLimitReached) {
|
if errors.Is(err, services.ErrPropertiesLimitReached) {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.properties_limit_reached")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
@@ -110,7 +111,7 @@ func (h *ResidenceHandler) UpdateResidence(c *gin.Context) {
|
|||||||
|
|
||||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +125,9 @@ func (h *ResidenceHandler) UpdateResidence(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrResidenceNotFound):
|
case errors.Is(err, services.ErrResidenceNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||||
case errors.Is(err, services.ErrNotResidenceOwner):
|
case errors.Is(err, services.ErrNotResidenceOwner):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -142,7 +143,7 @@ func (h *ResidenceHandler) DeleteResidence(c *gin.Context) {
|
|||||||
|
|
||||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,16 +151,16 @@ func (h *ResidenceHandler) DeleteResidence(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrResidenceNotFound):
|
case errors.Is(err, services.ErrResidenceNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||||
case errors.Is(err, services.ErrNotResidenceOwner):
|
case errors.Is(err, services.ErrNotResidenceOwner):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Residence deleted successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.residence_deleted")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateShareCode handles POST /api/residences/:id/generate-share-code/
|
// GenerateShareCode handles POST /api/residences/:id/generate-share-code/
|
||||||
@@ -168,7 +169,7 @@ func (h *ResidenceHandler) GenerateShareCode(c *gin.Context) {
|
|||||||
|
|
||||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,9 +181,9 @@ func (h *ResidenceHandler) GenerateShareCode(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrResidenceNotFound):
|
case errors.Is(err, services.ErrResidenceNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||||
case errors.Is(err, services.ErrNotResidenceOwner):
|
case errors.Is(err, services.ErrNotResidenceOwner):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -206,11 +207,11 @@ func (h *ResidenceHandler) JoinWithCode(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrShareCodeInvalid):
|
case errors.Is(err, services.ErrShareCodeInvalid):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.share_code_invalid")})
|
||||||
case errors.Is(err, services.ErrShareCodeExpired):
|
case errors.Is(err, services.ErrShareCodeExpired):
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.share_code_expired")})
|
||||||
case errors.Is(err, services.ErrUserAlreadyMember):
|
case errors.Is(err, services.ErrUserAlreadyMember):
|
||||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
c.JSON(http.StatusConflict, gin.H{"error": i18n.LocalizedMessage(c, "error.user_already_member")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -226,7 +227,7 @@ func (h *ResidenceHandler) GetResidenceUsers(c *gin.Context) {
|
|||||||
|
|
||||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,9 +235,9 @@ func (h *ResidenceHandler) GetResidenceUsers(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrResidenceNotFound):
|
case errors.Is(err, services.ErrResidenceNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||||
case errors.Is(err, services.ErrResidenceAccessDenied):
|
case errors.Is(err, services.ErrResidenceAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -252,13 +253,13 @@ func (h *ResidenceHandler) RemoveResidenceUser(c *gin.Context) {
|
|||||||
|
|
||||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userIDToRemove, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
|
userIDToRemove, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_user_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,18 +267,18 @@ func (h *ResidenceHandler) RemoveResidenceUser(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrResidenceNotFound):
|
case errors.Is(err, services.ErrResidenceNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||||
case errors.Is(err, services.ErrNotResidenceOwner):
|
case errors.Is(err, services.ErrNotResidenceOwner):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
|
||||||
case errors.Is(err, services.ErrCannotRemoveOwner):
|
case errors.Is(err, services.ErrCannotRemoveOwner):
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.cannot_remove_owner")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "User removed from residence"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.user_removed")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetResidenceTypes handles GET /api/residences/types/
|
// GetResidenceTypes handles GET /api/residences/types/
|
||||||
@@ -298,7 +299,7 @@ func (h *ResidenceHandler) GenerateTasksReport(c *gin.Context) {
|
|||||||
|
|
||||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,9 +314,9 @@ func (h *ResidenceHandler) GenerateTasksReport(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrResidenceNotFound):
|
case errors.Is(err, services.ErrResidenceNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||||
case errors.Is(err, services.ErrResidenceAccessDenied):
|
case errors.Is(err, services.ErrResidenceAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -360,11 +361,11 @@ func (h *ResidenceHandler) GenerateTasksReport(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build response message
|
// Build response message
|
||||||
message := "Tasks report generated successfully"
|
message := i18n.LocalizedMessage(c, "message.tasks_report_generated")
|
||||||
if pdfGenerated && emailSent {
|
if pdfGenerated && emailSent {
|
||||||
message = "Tasks report generated and sent to " + recipientEmail
|
message = i18n.LocalizedMessageWithData(c, "message.tasks_report_sent", map[string]interface{}{"Email": recipientEmail})
|
||||||
} else if pdfGenerated && !emailSent {
|
} else if pdfGenerated && !emailSent {
|
||||||
message = "Tasks report generated but email could not be sent"
|
message = i18n.LocalizedMessage(c, "message.tasks_report_email_failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,37 +35,37 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
|
|||||||
// Get all lookup data
|
// Get all lookup data
|
||||||
residenceTypes, err := h.residenceService.GetResidenceTypes()
|
residenceTypes, err := h.residenceService.GetResidenceTypes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch residence types"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_residence_types")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
taskCategories, err := h.taskService.GetCategories()
|
taskCategories, err := h.taskService.GetCategories()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task categories"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_categories")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
taskPriorities, err := h.taskService.GetPriorities()
|
taskPriorities, err := h.taskService.GetPriorities()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task priorities"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_priorities")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
taskFrequencies, err := h.taskService.GetFrequencies()
|
taskFrequencies, err := h.taskService.GetFrequencies()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task frequencies"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_frequencies")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
taskStatuses, err := h.taskService.GetStatuses()
|
taskStatuses, err := h.taskService.GetStatuses()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch task statuses"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_statuses")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
contractorSpecialties, err := h.contractorService.GetSpecialties()
|
contractorSpecialties, err := h.contractorService.GetSpecialties()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch contractor specialties"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_contractor_specialties")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
|
|||||||
// Kept for API compatibility with mobile clients
|
// Kept for API compatibility with mobile clients
|
||||||
func (h *StaticDataHandler) RefreshStaticData(c *gin.Context) {
|
func (h *StaticDataHandler) RefreshStaticData(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "Static data refreshed",
|
"message": i18n.LocalizedMessage(c, "message.static_data_refreshed"),
|
||||||
"status": "success",
|
"status": "success",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/middleware"
|
"github.com/treytartt/casera-api/internal/middleware"
|
||||||
"github.com/treytartt/casera-api/internal/models"
|
"github.com/treytartt/casera-api/internal/models"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
@@ -54,7 +55,7 @@ func (h *SubscriptionHandler) GetUpgradeTrigger(c *gin.Context) {
|
|||||||
trigger, err := h.subscriptionService.GetUpgradeTrigger(key)
|
trigger, err := h.subscriptionService.GetUpgradeTrigger(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrUpgradeTriggerNotFound) {
|
if errors.Is(err, services.ErrUpgradeTriggerNotFound) {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.upgrade_trigger_not_found")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
@@ -115,13 +116,13 @@ func (h *SubscriptionHandler) ProcessPurchase(c *gin.Context) {
|
|||||||
switch req.Platform {
|
switch req.Platform {
|
||||||
case "ios":
|
case "ios":
|
||||||
if req.ReceiptData == "" {
|
if req.ReceiptData == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "receipt_data is required for iOS"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.receipt_data_required")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
subscription, err = h.subscriptionService.ProcessApplePurchase(user.ID, req.ReceiptData)
|
subscription, err = h.subscriptionService.ProcessApplePurchase(user.ID, req.ReceiptData)
|
||||||
case "android":
|
case "android":
|
||||||
if req.PurchaseToken == "" {
|
if req.PurchaseToken == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "purchase_token is required for Android"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.purchase_token_required")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
subscription, err = h.subscriptionService.ProcessGooglePurchase(user.ID, req.PurchaseToken)
|
subscription, err = h.subscriptionService.ProcessGooglePurchase(user.ID, req.PurchaseToken)
|
||||||
@@ -133,7 +134,7 @@ func (h *SubscriptionHandler) ProcessPurchase(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "Subscription upgraded successfully",
|
"message": i18n.LocalizedMessage(c, "message.subscription_upgraded"),
|
||||||
"subscription": subscription,
|
"subscription": subscription,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,7 @@ func (h *SubscriptionHandler) CancelSubscription(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "Subscription cancelled. You will retain Pro benefits until the end of your billing period.",
|
"message": i18n.LocalizedMessage(c, "message.subscription_cancelled"),
|
||||||
"subscription": subscription,
|
"subscription": subscription,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -181,7 +182,7 @@ func (h *SubscriptionHandler) RestoreSubscription(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "Subscription restored successfully",
|
"message": i18n.LocalizedMessage(c, "message.subscription_restored"),
|
||||||
"subscription": subscription,
|
"subscription": subscription,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/middleware"
|
"github.com/treytartt/casera-api/internal/middleware"
|
||||||
"github.com/treytartt/casera-api/internal/models"
|
"github.com/treytartt/casera-api/internal/models"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
@@ -47,7 +48,7 @@ func (h *TaskHandler) GetTask(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +56,9 @@ func (h *TaskHandler) GetTask(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -71,7 +72,7 @@ func (h *TaskHandler) GetTasksByResidence(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32)
|
residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ func (h *TaskHandler) GetTasksByResidence(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrResidenceAccessDenied):
|
case errors.Is(err, services.ErrResidenceAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -107,7 +108,7 @@ func (h *TaskHandler) CreateTask(c *gin.Context) {
|
|||||||
response, err := h.taskService.CreateTask(&req, user.ID)
|
response, err := h.taskService.CreateTask(&req, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
@@ -121,7 +122,7 @@ func (h *TaskHandler) UpdateTask(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,9 +136,9 @@ func (h *TaskHandler) UpdateTask(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -151,7 +152,7 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,15 +160,15 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_deleted")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkInProgress handles POST /api/tasks/:id/mark-in-progress/
|
// MarkInProgress handles POST /api/tasks/:id/mark-in-progress/
|
||||||
@@ -175,7 +176,7 @@ func (h *TaskHandler) MarkInProgress(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,15 +184,15 @@ func (h *TaskHandler) MarkInProgress(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Task marked as in progress", "task": response})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_in_progress"), "task": response})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelTask handles POST /api/tasks/:id/cancel/
|
// CancelTask handles POST /api/tasks/:id/cancel/
|
||||||
@@ -199,7 +200,7 @@ func (h *TaskHandler) CancelTask(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,17 +208,17 @@ func (h *TaskHandler) CancelTask(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
case errors.Is(err, services.ErrTaskAlreadyCancelled):
|
case errors.Is(err, services.ErrTaskAlreadyCancelled):
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.task_already_cancelled")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Task cancelled", "task": response})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_cancelled"), "task": response})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UncancelTask handles POST /api/tasks/:id/uncancel/
|
// UncancelTask handles POST /api/tasks/:id/uncancel/
|
||||||
@@ -225,7 +226,7 @@ func (h *TaskHandler) UncancelTask(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,15 +234,15 @@ func (h *TaskHandler) UncancelTask(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Task uncancelled", "task": response})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_uncancelled"), "task": response})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArchiveTask handles POST /api/tasks/:id/archive/
|
// ArchiveTask handles POST /api/tasks/:id/archive/
|
||||||
@@ -249,7 +250,7 @@ func (h *TaskHandler) ArchiveTask(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,17 +258,17 @@ func (h *TaskHandler) ArchiveTask(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
case errors.Is(err, services.ErrTaskAlreadyArchived):
|
case errors.Is(err, services.ErrTaskAlreadyArchived):
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.task_already_archived")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Task archived", "task": response})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_archived"), "task": response})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnarchiveTask handles POST /api/tasks/:id/unarchive/
|
// UnarchiveTask handles POST /api/tasks/:id/unarchive/
|
||||||
@@ -275,7 +276,7 @@ func (h *TaskHandler) UnarchiveTask(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,15 +284,15 @@ func (h *TaskHandler) UnarchiveTask(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Task unarchived", "task": response})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_unarchived"), "task": response})
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Task Completions ===
|
// === Task Completions ===
|
||||||
@@ -301,7 +302,7 @@ func (h *TaskHandler) GetTaskCompletions(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,9 +310,9 @@ func (h *TaskHandler) GetTaskCompletions(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -336,7 +337,7 @@ func (h *TaskHandler) GetCompletion(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_completion_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,9 +345,9 @@ func (h *TaskHandler) GetCompletion(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrCompletionNotFound):
|
case errors.Is(err, services.ErrCompletionNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.completion_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -367,19 +368,19 @@ func (h *TaskHandler) CreateCompletion(c *gin.Context) {
|
|||||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||||
// Parse multipart form
|
// Parse multipart form
|
||||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to parse multipart form: " + err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_parse_form")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse task_id (required)
|
// Parse task_id (required)
|
||||||
taskIDStr := c.PostForm("task_id")
|
taskIDStr := c.PostForm("task_id")
|
||||||
if taskIDStr == "" {
|
if taskIDStr == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "task_id is required"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.task_id_required")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
|
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid task_id"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id_value")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.TaskID = uint(taskID)
|
req.TaskID = uint(taskID)
|
||||||
@@ -416,7 +417,7 @@ func (h *TaskHandler) CreateCompletion(c *gin.Context) {
|
|||||||
if h.storageService != nil {
|
if h.storageService != nil {
|
||||||
result, err := h.storageService.Upload(file, "completions")
|
result, err := h.storageService.Upload(file, "completions")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to upload image: " + err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_upload_image")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.ImageURLs = append(req.ImageURLs, result.URL)
|
req.ImageURLs = append(req.ImageURLs, result.URL)
|
||||||
@@ -434,9 +435,9 @@ func (h *TaskHandler) CreateCompletion(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -450,7 +451,7 @@ func (h *TaskHandler) DeleteCompletion(c *gin.Context) {
|
|||||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||||
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_completion_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,15 +459,15 @@ func (h *TaskHandler) DeleteCompletion(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrCompletionNotFound):
|
case errors.Is(err, services.ErrCompletionNotFound):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.completion_not_found")})
|
||||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Completion deleted successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.completion_deleted")})
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Lookups ===
|
// === Lookups ===
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ func NewUploadHandler(storageService *services.StorageService) *UploadHandler {
|
|||||||
func (h *UploadHandler) UploadImage(c *gin.Context) {
|
func (h *UploadHandler) UploadImage(c *gin.Context) {
|
||||||
file, err := c.FormFile("file")
|
file, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.no_file_provided")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ func (h *UploadHandler) UploadImage(c *gin.Context) {
|
|||||||
func (h *UploadHandler) UploadDocument(c *gin.Context) {
|
func (h *UploadHandler) UploadDocument(c *gin.Context) {
|
||||||
file, err := c.FormFile("file")
|
file, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.no_file_provided")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ func (h *UploadHandler) UploadDocument(c *gin.Context) {
|
|||||||
func (h *UploadHandler) UploadCompletion(c *gin.Context) {
|
func (h *UploadHandler) UploadCompletion(c *gin.Context) {
|
||||||
file, err := c.FormFile("file")
|
file, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.no_file_provided")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,5 +93,5 @@ func (h *UploadHandler) DeleteFile(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.file_deleted")})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/middleware"
|
"github.com/treytartt/casera-api/internal/middleware"
|
||||||
"github.com/treytartt/casera-api/internal/models"
|
"github.com/treytartt/casera-api/internal/models"
|
||||||
"github.com/treytartt/casera-api/internal/services"
|
"github.com/treytartt/casera-api/internal/services"
|
||||||
@@ -46,7 +47,7 @@ func (h *UserHandler) GetUser(c *gin.Context) {
|
|||||||
|
|
||||||
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_user_id")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ func (h *UserHandler) GetUser(c *gin.Context) {
|
|||||||
targetUser, err := h.userService.GetUserIfSharedResidence(uint(userID), user.ID)
|
targetUser, err := h.userService.GetUserIfSharedResidence(uint(userID), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == services.ErrUserNotFound {
|
if err == services.ErrUserNotFound {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.user_not_found")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
|||||||
86
internal/i18n/i18n.go
Normal file
86
internal/i18n/i18n.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed translations/*.json
|
||||||
|
var translationFS embed.FS
|
||||||
|
|
||||||
|
// Bundle is the global i18n bundle
|
||||||
|
var Bundle *i18n.Bundle
|
||||||
|
|
||||||
|
// SupportedLanguages lists all supported language codes
|
||||||
|
var SupportedLanguages = []string{"en", "es", "fr", "de", "pt"}
|
||||||
|
|
||||||
|
// DefaultLanguage is the fallback language
|
||||||
|
const DefaultLanguage = "en"
|
||||||
|
|
||||||
|
// Init initializes the i18n bundle with embedded translation files
|
||||||
|
func Init() error {
|
||||||
|
Bundle = i18n.NewBundle(language.English)
|
||||||
|
Bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
||||||
|
|
||||||
|
// Load all translation files
|
||||||
|
for _, lang := range SupportedLanguages {
|
||||||
|
path := "translations/" + lang + ".json"
|
||||||
|
data, err := translationFS.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Str("language", lang).Err(err).Msg("Failed to load translation file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Bundle.MustParseMessageFileBytes(data, path)
|
||||||
|
log.Info().Str("language", lang).Msg("Loaded translation file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocalizer creates a new localizer for the given language tags
|
||||||
|
func NewLocalizer(langs ...string) *i18n.Localizer {
|
||||||
|
return i18n.NewLocalizer(Bundle, langs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// T translates a message ID with optional template data
|
||||||
|
func T(localizer *i18n.Localizer, messageID string, templateData map[string]interface{}) string {
|
||||||
|
if localizer == nil {
|
||||||
|
localizer = NewLocalizer(DefaultLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||||
|
MessageID: messageID,
|
||||||
|
TemplateData: templateData,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to message ID if translation not found
|
||||||
|
log.Debug().Str("message_id", messageID).Err(err).Msg("Translation not found")
|
||||||
|
return messageID
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// TSimple translates a message ID without template data
|
||||||
|
func TSimple(localizer *i18n.Localizer, messageID string) string {
|
||||||
|
return T(localizer, messageID, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustT translates a message ID or panics
|
||||||
|
func MustT(localizer *i18n.Localizer, messageID string, templateData map[string]interface{}) string {
|
||||||
|
if localizer == nil {
|
||||||
|
localizer = NewLocalizer(DefaultLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||||
|
MessageID: messageID,
|
||||||
|
TemplateData: templateData,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
122
internal/i18n/middleware.go
Normal file
122
internal/i18n/middleware.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LocalizerKey is the key used to store the localizer in Gin context
|
||||||
|
LocalizerKey = "i18n_localizer"
|
||||||
|
// LocaleKey is the key used to store the detected locale in Gin context
|
||||||
|
LocaleKey = "i18n_locale"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Middleware returns a Gin middleware that detects the user's preferred language
|
||||||
|
// from the Accept-Language header and stores a localizer in the context
|
||||||
|
func Middleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Get Accept-Language header
|
||||||
|
acceptLang := c.GetHeader("Accept-Language")
|
||||||
|
|
||||||
|
// Parse the preferred languages
|
||||||
|
langs := parseAcceptLanguage(acceptLang)
|
||||||
|
|
||||||
|
// Create localizer with the preferred languages
|
||||||
|
localizer := NewLocalizer(langs...)
|
||||||
|
|
||||||
|
// Determine the best matched locale for storage
|
||||||
|
locale := matchLocale(langs)
|
||||||
|
|
||||||
|
// Store in context
|
||||||
|
c.Set(LocalizerKey, localizer)
|
||||||
|
c.Set(LocaleKey, locale)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAcceptLanguage parses the Accept-Language header and returns a slice of language tags
|
||||||
|
func parseAcceptLanguage(header string) []string {
|
||||||
|
if header == "" {
|
||||||
|
return []string{DefaultLanguage}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse using golang.org/x/text/language
|
||||||
|
tags, _, err := language.ParseAcceptLanguage(header)
|
||||||
|
if err != nil || len(tags) == 0 {
|
||||||
|
return []string{DefaultLanguage}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to string slice and normalize
|
||||||
|
langs := make([]string, 0, len(tags))
|
||||||
|
for _, tag := range tags {
|
||||||
|
base, _ := tag.Base()
|
||||||
|
lang := strings.ToLower(base.String())
|
||||||
|
|
||||||
|
// Only add supported languages
|
||||||
|
for _, supported := range SupportedLanguages {
|
||||||
|
if lang == supported {
|
||||||
|
langs = append(langs, lang)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no supported languages found, use default
|
||||||
|
if len(langs) == 0 {
|
||||||
|
return []string{DefaultLanguage}
|
||||||
|
}
|
||||||
|
|
||||||
|
return langs
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchLocale returns the best matching locale from the provided languages
|
||||||
|
func matchLocale(langs []string) string {
|
||||||
|
for _, lang := range langs {
|
||||||
|
for _, supported := range SupportedLanguages {
|
||||||
|
if lang == supported {
|
||||||
|
return supported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DefaultLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalizer retrieves the localizer from the Gin context
|
||||||
|
func GetLocalizer(c *gin.Context) *i18n.Localizer {
|
||||||
|
if localizer, exists := c.Get(LocalizerKey); exists {
|
||||||
|
if l, ok := localizer.(*i18n.Localizer); ok {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NewLocalizer(DefaultLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocale retrieves the detected locale from the Gin context
|
||||||
|
func GetLocale(c *gin.Context) string {
|
||||||
|
if locale, exists := c.Get(LocaleKey); exists {
|
||||||
|
if l, ok := locale.(string); ok {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DefaultLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalizedError returns a localized error message
|
||||||
|
func LocalizedError(c *gin.Context, messageID string, templateData map[string]interface{}) string {
|
||||||
|
return T(GetLocalizer(c), messageID, templateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalizedMessage returns a localized message
|
||||||
|
func LocalizedMessage(c *gin.Context, messageID string) string {
|
||||||
|
return TSimple(GetLocalizer(c), messageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalizedMessageWithData returns a localized message with template data
|
||||||
|
func LocalizedMessageWithData(c *gin.Context, messageID string, templateData map[string]interface{}) string {
|
||||||
|
return T(GetLocalizer(c), messageID, templateData)
|
||||||
|
}
|
||||||
187
internal/i18n/translations/de.json
Normal file
187
internal/i18n/translations/de.json
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
{
|
||||||
|
"error.invalid_request_body": "Ungultiger Anforderungstext",
|
||||||
|
"error.invalid_credentials": "Ungultige Anmeldedaten",
|
||||||
|
"error.account_inactive": "Das Konto ist inaktiv",
|
||||||
|
"error.username_taken": "Benutzername bereits vergeben",
|
||||||
|
"error.email_taken": "E-Mail bereits registriert",
|
||||||
|
"error.email_already_taken": "E-Mail bereits vergeben",
|
||||||
|
"error.registration_failed": "Registrierung fehlgeschlagen",
|
||||||
|
"error.not_authenticated": "Nicht authentifiziert",
|
||||||
|
"error.failed_to_get_user": "Benutzer konnte nicht abgerufen werden",
|
||||||
|
"error.failed_to_update_profile": "Profil konnte nicht aktualisiert werden",
|
||||||
|
"error.invalid_verification_code": "Ungultiger Verifizierungscode",
|
||||||
|
"error.verification_code_expired": "Der Verifizierungscode ist abgelaufen",
|
||||||
|
"error.email_already_verified": "E-Mail bereits verifiziert",
|
||||||
|
"error.verification_failed": "Verifizierung fehlgeschlagen",
|
||||||
|
"error.failed_to_resend_verification": "Verifizierung konnte nicht erneut gesendet werden",
|
||||||
|
"error.rate_limit_exceeded": "Zu viele Anfragen zur Passwortzurucksetzung. Bitte versuchen Sie es spater erneut.",
|
||||||
|
"error.too_many_attempts": "Zu viele Versuche. Bitte fordern Sie einen neuen Code an.",
|
||||||
|
"error.invalid_reset_token": "Ungultiger oder abgelaufener Zurucksetzungs-Token",
|
||||||
|
"error.password_reset_failed": "Passwortzurucksetzung fehlgeschlagen",
|
||||||
|
"error.apple_signin_not_configured": "Apple-Anmeldung ist nicht konfiguriert",
|
||||||
|
"error.apple_signin_failed": "Apple-Anmeldung fehlgeschlagen",
|
||||||
|
"error.invalid_apple_token": "Ungultiger Apple-Identitats-Token",
|
||||||
|
|
||||||
|
"error.invalid_task_id": "Ungultige Aufgaben-ID",
|
||||||
|
"error.invalid_residence_id": "Ungultige Immobilien-ID",
|
||||||
|
"error.invalid_contractor_id": "Ungultige Dienstleister-ID",
|
||||||
|
"error.invalid_document_id": "Ungultige Dokument-ID",
|
||||||
|
"error.invalid_completion_id": "Ungultige Abschluss-ID",
|
||||||
|
"error.invalid_user_id": "Ungultige Benutzer-ID",
|
||||||
|
"error.invalid_notification_id": "Ungultige Benachrichtigungs-ID",
|
||||||
|
"error.invalid_device_id": "Ungultige Gerate-ID",
|
||||||
|
|
||||||
|
"error.task_not_found": "Aufgabe nicht gefunden",
|
||||||
|
"error.residence_not_found": "Immobilie nicht gefunden",
|
||||||
|
"error.contractor_not_found": "Dienstleister nicht gefunden",
|
||||||
|
"error.document_not_found": "Dokument nicht gefunden",
|
||||||
|
"error.completion_not_found": "Aufgabenabschluss nicht gefunden",
|
||||||
|
"error.user_not_found": "Benutzer nicht gefunden",
|
||||||
|
"error.share_code_invalid": "Ungultiger Freigabecode",
|
||||||
|
"error.share_code_expired": "Der Freigabecode ist abgelaufen",
|
||||||
|
|
||||||
|
"error.task_access_denied": "Sie haben keinen Zugriff auf diese Aufgabe",
|
||||||
|
"error.residence_access_denied": "Sie haben keinen Zugriff auf diese Immobilie",
|
||||||
|
"error.contractor_access_denied": "Sie haben keinen Zugriff auf diesen Dienstleister",
|
||||||
|
"error.document_access_denied": "Sie haben keinen Zugriff auf dieses Dokument",
|
||||||
|
"error.not_residence_owner": "Nur der Eigentumer kann diese Aktion durchfuhren",
|
||||||
|
"error.cannot_remove_owner": "Der Eigentumer kann nicht entfernt werden",
|
||||||
|
"error.user_already_member": "Der Benutzer ist bereits Mitglied dieser Immobilie",
|
||||||
|
"error.properties_limit_reached": "Sie haben die maximale Anzahl an Immobilien fur Ihr Abonnement erreicht",
|
||||||
|
|
||||||
|
"error.task_already_cancelled": "Die Aufgabe ist bereits storniert",
|
||||||
|
"error.task_already_archived": "Die Aufgabe ist bereits archiviert",
|
||||||
|
|
||||||
|
"error.failed_to_parse_form": "Formular konnte nicht analysiert werden",
|
||||||
|
"error.task_id_required": "task_id ist erforderlich",
|
||||||
|
"error.invalid_task_id_value": "Ungultige task_id",
|
||||||
|
"error.failed_to_upload_image": "Bild konnte nicht hochgeladen werden",
|
||||||
|
"error.residence_id_required": "residence_id ist erforderlich",
|
||||||
|
"error.invalid_residence_id_value": "Ungultige residence_id",
|
||||||
|
"error.title_required": "Titel ist erforderlich",
|
||||||
|
"error.failed_to_upload_file": "Datei konnte nicht hochgeladen werden",
|
||||||
|
|
||||||
|
"message.logged_out": "Erfolgreich abgemeldet",
|
||||||
|
"message.email_verified": "E-Mail erfolgreich verifiziert",
|
||||||
|
"message.verification_email_sent": "Verifizierungs-E-Mail gesendet",
|
||||||
|
"message.password_reset_email_sent": "Wenn ein Konto mit dieser E-Mail existiert, wurde ein Zurucksetzungscode gesendet.",
|
||||||
|
"message.reset_code_verified": "Code erfolgreich verifiziert",
|
||||||
|
"message.password_reset_success": "Passwort erfolgreich zuruckgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an.",
|
||||||
|
|
||||||
|
"message.task_deleted": "Aufgabe erfolgreich geloscht",
|
||||||
|
"message.task_in_progress": "Aufgabe als in Bearbeitung markiert",
|
||||||
|
"message.task_cancelled": "Aufgabe storniert",
|
||||||
|
"message.task_uncancelled": "Aufgabe reaktiviert",
|
||||||
|
"message.task_archived": "Aufgabe archiviert",
|
||||||
|
"message.task_unarchived": "Aufgabe dearchiviert",
|
||||||
|
"message.completion_deleted": "Abschluss erfolgreich geloscht",
|
||||||
|
|
||||||
|
"message.residence_deleted": "Immobilie erfolgreich geloscht",
|
||||||
|
"message.user_removed": "Benutzer von der Immobilie entfernt",
|
||||||
|
"message.tasks_report_generated": "Aufgabenbericht erfolgreich erstellt",
|
||||||
|
"message.tasks_report_sent": "Aufgabenbericht erstellt und an {{.Email}} gesendet",
|
||||||
|
"message.tasks_report_email_failed": "Aufgabenbericht erstellt, aber E-Mail konnte nicht gesendet werden",
|
||||||
|
|
||||||
|
"message.contractor_deleted": "Dienstleister erfolgreich geloscht",
|
||||||
|
|
||||||
|
"message.document_deleted": "Dokument erfolgreich geloscht",
|
||||||
|
"message.document_activated": "Dokument aktiviert",
|
||||||
|
"message.document_deactivated": "Dokument deaktiviert",
|
||||||
|
|
||||||
|
"message.notification_marked_read": "Benachrichtigung als gelesen markiert",
|
||||||
|
"message.all_notifications_marked_read": "Alle Benachrichtigungen als gelesen markiert",
|
||||||
|
"message.device_removed": "Gerät entfernt",
|
||||||
|
|
||||||
|
"message.subscription_upgraded": "Abonnement erfolgreich aktualisiert",
|
||||||
|
"message.subscription_cancelled": "Abonnement gekündigt. Sie behalten die Pro-Vorteile bis zum Ende Ihres Abrechnungszeitraums.",
|
||||||
|
"message.subscription_restored": "Abonnement erfolgreich wiederhergestellt",
|
||||||
|
|
||||||
|
"message.file_deleted": "Datei erfolgreich gelöscht",
|
||||||
|
"message.static_data_refreshed": "Statische Daten aktualisiert",
|
||||||
|
|
||||||
|
"error.notification_not_found": "Benachrichtigung nicht gefunden",
|
||||||
|
"error.invalid_platform": "Ungültige Plattform",
|
||||||
|
|
||||||
|
"error.upgrade_trigger_not_found": "Upgrade-Trigger nicht gefunden",
|
||||||
|
"error.receipt_data_required": "receipt_data ist für iOS erforderlich",
|
||||||
|
"error.purchase_token_required": "purchase_token ist für Android erforderlich",
|
||||||
|
|
||||||
|
"error.no_file_provided": "Keine Datei bereitgestellt",
|
||||||
|
|
||||||
|
"error.failed_to_fetch_residence_types": "Fehler beim Abrufen der Immobilientypen",
|
||||||
|
"error.failed_to_fetch_task_categories": "Fehler beim Abrufen der Aufgabenkategorien",
|
||||||
|
"error.failed_to_fetch_task_priorities": "Fehler beim Abrufen der Aufgabenprioritäten",
|
||||||
|
"error.failed_to_fetch_task_frequencies": "Fehler beim Abrufen der Aufgabenfrequenzen",
|
||||||
|
"error.failed_to_fetch_task_statuses": "Fehler beim Abrufen der Aufgabenstatus",
|
||||||
|
"error.failed_to_fetch_contractor_specialties": "Fehler beim Abrufen der Dienstleister-Spezialitäten",
|
||||||
|
|
||||||
|
"push.task_due_soon.title": "Aufgabe Bald Fallig",
|
||||||
|
"push.task_due_soon.body": "{{.TaskTitle}} ist fallig am {{.DueDate}}",
|
||||||
|
"push.task_overdue.title": "Uberfällige Aufgabe",
|
||||||
|
"push.task_overdue.body": "{{.TaskTitle}} ist uberfallig",
|
||||||
|
"push.task_completed.title": "Aufgabe Abgeschlossen",
|
||||||
|
"push.task_completed.body": "{{.UserName}} hat {{.TaskTitle}} abgeschlossen",
|
||||||
|
"push.task_assigned.title": "Neue Aufgabe Zugewiesen",
|
||||||
|
"push.task_assigned.body": "Ihnen wurde {{.TaskTitle}} zugewiesen",
|
||||||
|
"push.residence_shared.title": "Immobilie Geteilt",
|
||||||
|
"push.residence_shared.body": "{{.UserName}} hat {{.ResidenceName}} mit Ihnen geteilt",
|
||||||
|
|
||||||
|
"email.welcome.subject": "Willkommen bei Casera!",
|
||||||
|
"email.verification.subject": "Bestatigen Sie Ihre E-Mail",
|
||||||
|
"email.password_reset.subject": "Passwort-Zurucksetzungscode",
|
||||||
|
"email.tasks_report.subject": "Aufgabenbericht fur {{.ResidenceName}}",
|
||||||
|
|
||||||
|
"lookup.residence_type.house": "Haus",
|
||||||
|
"lookup.residence_type.apartment": "Wohnung",
|
||||||
|
"lookup.residence_type.condo": "Eigentumswohnung",
|
||||||
|
"lookup.residence_type.townhouse": "Reihenhaus",
|
||||||
|
"lookup.residence_type.mobile_home": "Mobilheim",
|
||||||
|
"lookup.residence_type.other": "Sonstiges",
|
||||||
|
|
||||||
|
"lookup.task_category.plumbing": "Sanitär",
|
||||||
|
"lookup.task_category.electrical": "Elektrik",
|
||||||
|
"lookup.task_category.hvac": "Heizung/Klimaanlage",
|
||||||
|
"lookup.task_category.appliances": "Gerate",
|
||||||
|
"lookup.task_category.exterior": "Aussenbereich",
|
||||||
|
"lookup.task_category.interior": "Innenbereich",
|
||||||
|
"lookup.task_category.landscaping": "Gartenpflege",
|
||||||
|
"lookup.task_category.safety": "Sicherheit",
|
||||||
|
"lookup.task_category.cleaning": "Reinigung",
|
||||||
|
"lookup.task_category.pest_control": "Schadlingsbekampfung",
|
||||||
|
"lookup.task_category.seasonal": "Saisonal",
|
||||||
|
"lookup.task_category.other": "Sonstiges",
|
||||||
|
|
||||||
|
"lookup.task_priority.low": "Niedrig",
|
||||||
|
"lookup.task_priority.medium": "Mittel",
|
||||||
|
"lookup.task_priority.high": "Hoch",
|
||||||
|
"lookup.task_priority.urgent": "Dringend",
|
||||||
|
|
||||||
|
"lookup.task_status.pending": "Ausstehend",
|
||||||
|
"lookup.task_status.in_progress": "In Bearbeitung",
|
||||||
|
"lookup.task_status.completed": "Abgeschlossen",
|
||||||
|
"lookup.task_status.cancelled": "Storniert",
|
||||||
|
"lookup.task_status.archived": "Archiviert",
|
||||||
|
|
||||||
|
"lookup.task_frequency.once": "Einmalig",
|
||||||
|
"lookup.task_frequency.daily": "Taglich",
|
||||||
|
"lookup.task_frequency.weekly": "Wochentlich",
|
||||||
|
"lookup.task_frequency.biweekly": "Alle 2 Wochen",
|
||||||
|
"lookup.task_frequency.monthly": "Monatlich",
|
||||||
|
"lookup.task_frequency.quarterly": "Vierteljahrlich",
|
||||||
|
"lookup.task_frequency.semiannually": "Halbjahrlich",
|
||||||
|
"lookup.task_frequency.annually": "Jahrlich",
|
||||||
|
|
||||||
|
"lookup.contractor_specialty.plumber": "Klempner",
|
||||||
|
"lookup.contractor_specialty.electrician": "Elektriker",
|
||||||
|
"lookup.contractor_specialty.hvac_technician": "HLK-Techniker",
|
||||||
|
"lookup.contractor_specialty.handyman": "Handwerker",
|
||||||
|
"lookup.contractor_specialty.landscaper": "Landschaftsgartner",
|
||||||
|
"lookup.contractor_specialty.roofer": "Dachdecker",
|
||||||
|
"lookup.contractor_specialty.painter": "Maler",
|
||||||
|
"lookup.contractor_specialty.carpenter": "Schreiner",
|
||||||
|
"lookup.contractor_specialty.pest_control": "Schadlingsbekampfung",
|
||||||
|
"lookup.contractor_specialty.cleaning": "Reinigung",
|
||||||
|
"lookup.contractor_specialty.pool_service": "Pool-Service",
|
||||||
|
"lookup.contractor_specialty.general_contractor": "Generalunternehmer",
|
||||||
|
"lookup.contractor_specialty.other": "Sonstiges"
|
||||||
|
}
|
||||||
187
internal/i18n/translations/en.json
Normal file
187
internal/i18n/translations/en.json
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
{
|
||||||
|
"error.invalid_request_body": "Invalid request body",
|
||||||
|
"error.invalid_credentials": "Invalid credentials",
|
||||||
|
"error.account_inactive": "Account is inactive",
|
||||||
|
"error.username_taken": "Username already taken",
|
||||||
|
"error.email_taken": "Email already registered",
|
||||||
|
"error.email_already_taken": "Email already taken",
|
||||||
|
"error.registration_failed": "Registration failed",
|
||||||
|
"error.not_authenticated": "Not authenticated",
|
||||||
|
"error.failed_to_get_user": "Failed to get user",
|
||||||
|
"error.failed_to_update_profile": "Failed to update profile",
|
||||||
|
"error.invalid_verification_code": "Invalid verification code",
|
||||||
|
"error.verification_code_expired": "Verification code has expired",
|
||||||
|
"error.email_already_verified": "Email already verified",
|
||||||
|
"error.verification_failed": "Verification failed",
|
||||||
|
"error.failed_to_resend_verification": "Failed to resend verification",
|
||||||
|
"error.rate_limit_exceeded": "Too many password reset requests. Please try again later.",
|
||||||
|
"error.too_many_attempts": "Too many attempts. Please request a new code.",
|
||||||
|
"error.invalid_reset_token": "Invalid or expired reset token",
|
||||||
|
"error.password_reset_failed": "Password reset failed",
|
||||||
|
"error.apple_signin_not_configured": "Apple Sign In is not configured",
|
||||||
|
"error.apple_signin_failed": "Apple Sign In failed",
|
||||||
|
"error.invalid_apple_token": "Invalid Apple identity token",
|
||||||
|
|
||||||
|
"error.invalid_task_id": "Invalid task ID",
|
||||||
|
"error.invalid_residence_id": "Invalid residence ID",
|
||||||
|
"error.invalid_contractor_id": "Invalid contractor ID",
|
||||||
|
"error.invalid_document_id": "Invalid document ID",
|
||||||
|
"error.invalid_completion_id": "Invalid completion ID",
|
||||||
|
"error.invalid_user_id": "Invalid user ID",
|
||||||
|
"error.invalid_notification_id": "Invalid notification ID",
|
||||||
|
"error.invalid_device_id": "Invalid device ID",
|
||||||
|
|
||||||
|
"error.task_not_found": "Task not found",
|
||||||
|
"error.residence_not_found": "Residence not found",
|
||||||
|
"error.contractor_not_found": "Contractor not found",
|
||||||
|
"error.document_not_found": "Document not found",
|
||||||
|
"error.completion_not_found": "Task completion not found",
|
||||||
|
"error.user_not_found": "User not found",
|
||||||
|
"error.share_code_invalid": "Invalid share code",
|
||||||
|
"error.share_code_expired": "Share code has expired",
|
||||||
|
|
||||||
|
"error.task_access_denied": "You don't have access to this task",
|
||||||
|
"error.residence_access_denied": "You don't have access to this property",
|
||||||
|
"error.contractor_access_denied": "You don't have access to this contractor",
|
||||||
|
"error.document_access_denied": "You don't have access to this document",
|
||||||
|
"error.not_residence_owner": "Only the property owner can perform this action",
|
||||||
|
"error.cannot_remove_owner": "Cannot remove the property owner",
|
||||||
|
"error.user_already_member": "User is already a member of this property",
|
||||||
|
"error.properties_limit_reached": "You have reached the maximum number of properties for your subscription",
|
||||||
|
|
||||||
|
"error.task_already_cancelled": "Task is already cancelled",
|
||||||
|
"error.task_already_archived": "Task is already archived",
|
||||||
|
|
||||||
|
"error.failed_to_parse_form": "Failed to parse multipart form",
|
||||||
|
"error.task_id_required": "task_id is required",
|
||||||
|
"error.invalid_task_id_value": "Invalid task_id",
|
||||||
|
"error.failed_to_upload_image": "Failed to upload image",
|
||||||
|
"error.residence_id_required": "residence_id is required",
|
||||||
|
"error.invalid_residence_id_value": "Invalid residence_id",
|
||||||
|
"error.title_required": "title is required",
|
||||||
|
"error.failed_to_upload_file": "Failed to upload file",
|
||||||
|
|
||||||
|
"message.logged_out": "Logged out successfully",
|
||||||
|
"message.email_verified": "Email verified successfully",
|
||||||
|
"message.verification_email_sent": "Verification email sent",
|
||||||
|
"message.password_reset_email_sent": "If an account with that email exists, a password reset code has been sent.",
|
||||||
|
"message.reset_code_verified": "Code verified successfully",
|
||||||
|
"message.password_reset_success": "Password reset successfully. Please log in with your new password.",
|
||||||
|
|
||||||
|
"message.task_deleted": "Task deleted successfully",
|
||||||
|
"message.task_in_progress": "Task marked as in progress",
|
||||||
|
"message.task_cancelled": "Task cancelled",
|
||||||
|
"message.task_uncancelled": "Task uncancelled",
|
||||||
|
"message.task_archived": "Task archived",
|
||||||
|
"message.task_unarchived": "Task unarchived",
|
||||||
|
"message.completion_deleted": "Completion deleted successfully",
|
||||||
|
|
||||||
|
"message.residence_deleted": "Residence deleted successfully",
|
||||||
|
"message.user_removed": "User removed from residence",
|
||||||
|
"message.tasks_report_generated": "Tasks report generated successfully",
|
||||||
|
"message.tasks_report_sent": "Tasks report generated and sent to {{.Email}}",
|
||||||
|
"message.tasks_report_email_failed": "Tasks report generated but email could not be sent",
|
||||||
|
|
||||||
|
"message.contractor_deleted": "Contractor deleted successfully",
|
||||||
|
|
||||||
|
"message.document_deleted": "Document deleted successfully",
|
||||||
|
"message.document_activated": "Document activated",
|
||||||
|
"message.document_deactivated": "Document deactivated",
|
||||||
|
|
||||||
|
"message.notification_marked_read": "Notification marked as read",
|
||||||
|
"message.all_notifications_marked_read": "All notifications marked as read",
|
||||||
|
"message.device_removed": "Device removed",
|
||||||
|
|
||||||
|
"message.subscription_upgraded": "Subscription upgraded successfully",
|
||||||
|
"message.subscription_cancelled": "Subscription cancelled. You will retain Pro benefits until the end of your billing period.",
|
||||||
|
"message.subscription_restored": "Subscription restored successfully",
|
||||||
|
|
||||||
|
"message.file_deleted": "File deleted successfully",
|
||||||
|
"message.static_data_refreshed": "Static data refreshed",
|
||||||
|
|
||||||
|
"error.notification_not_found": "Notification not found",
|
||||||
|
"error.invalid_platform": "Invalid platform",
|
||||||
|
|
||||||
|
"error.upgrade_trigger_not_found": "Upgrade trigger not found",
|
||||||
|
"error.receipt_data_required": "receipt_data is required for iOS",
|
||||||
|
"error.purchase_token_required": "purchase_token is required for Android",
|
||||||
|
|
||||||
|
"error.no_file_provided": "No file provided",
|
||||||
|
|
||||||
|
"error.failed_to_fetch_residence_types": "Failed to fetch residence types",
|
||||||
|
"error.failed_to_fetch_task_categories": "Failed to fetch task categories",
|
||||||
|
"error.failed_to_fetch_task_priorities": "Failed to fetch task priorities",
|
||||||
|
"error.failed_to_fetch_task_frequencies": "Failed to fetch task frequencies",
|
||||||
|
"error.failed_to_fetch_task_statuses": "Failed to fetch task statuses",
|
||||||
|
"error.failed_to_fetch_contractor_specialties": "Failed to fetch contractor specialties",
|
||||||
|
|
||||||
|
"push.task_due_soon.title": "Task Due Soon",
|
||||||
|
"push.task_due_soon.body": "{{.TaskTitle}} is due {{.DueDate}}",
|
||||||
|
"push.task_overdue.title": "Overdue Task",
|
||||||
|
"push.task_overdue.body": "{{.TaskTitle}} is overdue",
|
||||||
|
"push.task_completed.title": "Task Completed",
|
||||||
|
"push.task_completed.body": "{{.UserName}} completed {{.TaskTitle}}",
|
||||||
|
"push.task_assigned.title": "New Task Assigned",
|
||||||
|
"push.task_assigned.body": "You have been assigned to {{.TaskTitle}}",
|
||||||
|
"push.residence_shared.title": "Property Shared",
|
||||||
|
"push.residence_shared.body": "{{.UserName}} shared {{.ResidenceName}} with you",
|
||||||
|
|
||||||
|
"email.welcome.subject": "Welcome to Casera!",
|
||||||
|
"email.verification.subject": "Verify Your Email",
|
||||||
|
"email.password_reset.subject": "Password Reset Code",
|
||||||
|
"email.tasks_report.subject": "Tasks Report for {{.ResidenceName}}",
|
||||||
|
|
||||||
|
"lookup.residence_type.house": "House",
|
||||||
|
"lookup.residence_type.apartment": "Apartment",
|
||||||
|
"lookup.residence_type.condo": "Condo",
|
||||||
|
"lookup.residence_type.townhouse": "Townhouse",
|
||||||
|
"lookup.residence_type.mobile_home": "Mobile Home",
|
||||||
|
"lookup.residence_type.other": "Other",
|
||||||
|
|
||||||
|
"lookup.task_category.plumbing": "Plumbing",
|
||||||
|
"lookup.task_category.electrical": "Electrical",
|
||||||
|
"lookup.task_category.hvac": "HVAC",
|
||||||
|
"lookup.task_category.appliances": "Appliances",
|
||||||
|
"lookup.task_category.exterior": "Exterior",
|
||||||
|
"lookup.task_category.interior": "Interior",
|
||||||
|
"lookup.task_category.landscaping": "Landscaping",
|
||||||
|
"lookup.task_category.safety": "Safety",
|
||||||
|
"lookup.task_category.cleaning": "Cleaning",
|
||||||
|
"lookup.task_category.pest_control": "Pest Control",
|
||||||
|
"lookup.task_category.seasonal": "Seasonal",
|
||||||
|
"lookup.task_category.other": "Other",
|
||||||
|
|
||||||
|
"lookup.task_priority.low": "Low",
|
||||||
|
"lookup.task_priority.medium": "Medium",
|
||||||
|
"lookup.task_priority.high": "High",
|
||||||
|
"lookup.task_priority.urgent": "Urgent",
|
||||||
|
|
||||||
|
"lookup.task_status.pending": "Pending",
|
||||||
|
"lookup.task_status.in_progress": "In Progress",
|
||||||
|
"lookup.task_status.completed": "Completed",
|
||||||
|
"lookup.task_status.cancelled": "Cancelled",
|
||||||
|
"lookup.task_status.archived": "Archived",
|
||||||
|
|
||||||
|
"lookup.task_frequency.once": "Once",
|
||||||
|
"lookup.task_frequency.daily": "Daily",
|
||||||
|
"lookup.task_frequency.weekly": "Weekly",
|
||||||
|
"lookup.task_frequency.biweekly": "Every 2 Weeks",
|
||||||
|
"lookup.task_frequency.monthly": "Monthly",
|
||||||
|
"lookup.task_frequency.quarterly": "Quarterly",
|
||||||
|
"lookup.task_frequency.semiannually": "Every 6 Months",
|
||||||
|
"lookup.task_frequency.annually": "Annually",
|
||||||
|
|
||||||
|
"lookup.contractor_specialty.plumber": "Plumber",
|
||||||
|
"lookup.contractor_specialty.electrician": "Electrician",
|
||||||
|
"lookup.contractor_specialty.hvac_technician": "HVAC Technician",
|
||||||
|
"lookup.contractor_specialty.handyman": "Handyman",
|
||||||
|
"lookup.contractor_specialty.landscaper": "Landscaper",
|
||||||
|
"lookup.contractor_specialty.roofer": "Roofer",
|
||||||
|
"lookup.contractor_specialty.painter": "Painter",
|
||||||
|
"lookup.contractor_specialty.carpenter": "Carpenter",
|
||||||
|
"lookup.contractor_specialty.pest_control": "Pest Control",
|
||||||
|
"lookup.contractor_specialty.cleaning": "Cleaning",
|
||||||
|
"lookup.contractor_specialty.pool_service": "Pool Service",
|
||||||
|
"lookup.contractor_specialty.general_contractor": "General Contractor",
|
||||||
|
"lookup.contractor_specialty.other": "Other"
|
||||||
|
}
|
||||||
187
internal/i18n/translations/es.json
Normal file
187
internal/i18n/translations/es.json
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
{
|
||||||
|
"error.invalid_request_body": "Cuerpo de solicitud no valido",
|
||||||
|
"error.invalid_credentials": "Credenciales no validas",
|
||||||
|
"error.account_inactive": "La cuenta esta inactiva",
|
||||||
|
"error.username_taken": "El nombre de usuario ya esta en uso",
|
||||||
|
"error.email_taken": "El correo electronico ya esta registrado",
|
||||||
|
"error.email_already_taken": "El correo electronico ya esta en uso",
|
||||||
|
"error.registration_failed": "Error en el registro",
|
||||||
|
"error.not_authenticated": "No autenticado",
|
||||||
|
"error.failed_to_get_user": "Error al obtener el usuario",
|
||||||
|
"error.failed_to_update_profile": "Error al actualizar el perfil",
|
||||||
|
"error.invalid_verification_code": "Codigo de verificacion no valido",
|
||||||
|
"error.verification_code_expired": "El codigo de verificacion ha expirado",
|
||||||
|
"error.email_already_verified": "El correo electronico ya esta verificado",
|
||||||
|
"error.verification_failed": "Error en la verificacion",
|
||||||
|
"error.failed_to_resend_verification": "Error al reenviar la verificacion",
|
||||||
|
"error.rate_limit_exceeded": "Demasiadas solicitudes de restablecimiento de contrasena. Por favor, intentelo mas tarde.",
|
||||||
|
"error.too_many_attempts": "Demasiados intentos. Por favor, solicite un nuevo codigo.",
|
||||||
|
"error.invalid_reset_token": "Token de restablecimiento no valido o expirado",
|
||||||
|
"error.password_reset_failed": "Error al restablecer la contrasena",
|
||||||
|
"error.apple_signin_not_configured": "El inicio de sesion con Apple no esta configurado",
|
||||||
|
"error.apple_signin_failed": "Error en el inicio de sesion con Apple",
|
||||||
|
"error.invalid_apple_token": "Token de identidad de Apple no valido",
|
||||||
|
|
||||||
|
"error.invalid_task_id": "ID de tarea no valido",
|
||||||
|
"error.invalid_residence_id": "ID de propiedad no valido",
|
||||||
|
"error.invalid_contractor_id": "ID de contratista no valido",
|
||||||
|
"error.invalid_document_id": "ID de documento no valido",
|
||||||
|
"error.invalid_completion_id": "ID de finalizacion no valido",
|
||||||
|
"error.invalid_user_id": "ID de usuario no valido",
|
||||||
|
"error.invalid_notification_id": "ID de notificacion no valido",
|
||||||
|
"error.invalid_device_id": "ID de dispositivo no valido",
|
||||||
|
|
||||||
|
"error.task_not_found": "Tarea no encontrada",
|
||||||
|
"error.residence_not_found": "Propiedad no encontrada",
|
||||||
|
"error.contractor_not_found": "Contratista no encontrado",
|
||||||
|
"error.document_not_found": "Documento no encontrado",
|
||||||
|
"error.completion_not_found": "Finalizacion de tarea no encontrada",
|
||||||
|
"error.user_not_found": "Usuario no encontrado",
|
||||||
|
"error.share_code_invalid": "Codigo de compartir no valido",
|
||||||
|
"error.share_code_expired": "El codigo de compartir ha expirado",
|
||||||
|
|
||||||
|
"error.task_access_denied": "No tienes acceso a esta tarea",
|
||||||
|
"error.residence_access_denied": "No tienes acceso a esta propiedad",
|
||||||
|
"error.contractor_access_denied": "No tienes acceso a este contratista",
|
||||||
|
"error.document_access_denied": "No tienes acceso a este documento",
|
||||||
|
"error.not_residence_owner": "Solo el propietario de la propiedad puede realizar esta accion",
|
||||||
|
"error.cannot_remove_owner": "No se puede eliminar al propietario de la propiedad",
|
||||||
|
"error.user_already_member": "El usuario ya es miembro de esta propiedad",
|
||||||
|
"error.properties_limit_reached": "Has alcanzado el numero maximo de propiedades para tu suscripcion",
|
||||||
|
|
||||||
|
"error.task_already_cancelled": "La tarea ya esta cancelada",
|
||||||
|
"error.task_already_archived": "La tarea ya esta archivada",
|
||||||
|
|
||||||
|
"error.failed_to_parse_form": "Error al analizar el formulario",
|
||||||
|
"error.task_id_required": "Se requiere task_id",
|
||||||
|
"error.invalid_task_id_value": "task_id no valido",
|
||||||
|
"error.failed_to_upload_image": "Error al subir la imagen",
|
||||||
|
"error.residence_id_required": "Se requiere residence_id",
|
||||||
|
"error.invalid_residence_id_value": "residence_id no valido",
|
||||||
|
"error.title_required": "Se requiere el titulo",
|
||||||
|
"error.failed_to_upload_file": "Error al subir el archivo",
|
||||||
|
|
||||||
|
"message.logged_out": "Sesion cerrada correctamente",
|
||||||
|
"message.email_verified": "Correo electronico verificado correctamente",
|
||||||
|
"message.verification_email_sent": "Correo de verificacion enviado",
|
||||||
|
"message.password_reset_email_sent": "Si existe una cuenta con ese correo electronico, se ha enviado un codigo de restablecimiento de contrasena.",
|
||||||
|
"message.reset_code_verified": "Codigo verificado correctamente",
|
||||||
|
"message.password_reset_success": "Contrasena restablecida correctamente. Por favor, inicia sesion con tu nueva contrasena.",
|
||||||
|
|
||||||
|
"message.task_deleted": "Tarea eliminada correctamente",
|
||||||
|
"message.task_in_progress": "Tarea marcada como en progreso",
|
||||||
|
"message.task_cancelled": "Tarea cancelada",
|
||||||
|
"message.task_uncancelled": "Tarea reactivada",
|
||||||
|
"message.task_archived": "Tarea archivada",
|
||||||
|
"message.task_unarchived": "Tarea desarchivada",
|
||||||
|
"message.completion_deleted": "Finalizacion eliminada correctamente",
|
||||||
|
|
||||||
|
"message.residence_deleted": "Propiedad eliminada correctamente",
|
||||||
|
"message.user_removed": "Usuario eliminado de la propiedad",
|
||||||
|
"message.tasks_report_generated": "Informe de tareas generado correctamente",
|
||||||
|
"message.tasks_report_sent": "Informe de tareas generado y enviado a {{.Email}}",
|
||||||
|
"message.tasks_report_email_failed": "Informe de tareas generado pero no se pudo enviar el correo",
|
||||||
|
|
||||||
|
"message.contractor_deleted": "Contratista eliminado correctamente",
|
||||||
|
|
||||||
|
"message.document_deleted": "Documento eliminado correctamente",
|
||||||
|
"message.document_activated": "Documento activado",
|
||||||
|
"message.document_deactivated": "Documento desactivado",
|
||||||
|
|
||||||
|
"message.notification_marked_read": "Notificación marcada como leída",
|
||||||
|
"message.all_notifications_marked_read": "Todas las notificaciones marcadas como leídas",
|
||||||
|
"message.device_removed": "Dispositivo eliminado",
|
||||||
|
|
||||||
|
"message.subscription_upgraded": "Suscripción actualizada correctamente",
|
||||||
|
"message.subscription_cancelled": "Suscripción cancelada. Mantendrás los beneficios Pro hasta el final de tu período de facturación.",
|
||||||
|
"message.subscription_restored": "Suscripción restaurada correctamente",
|
||||||
|
|
||||||
|
"message.file_deleted": "Archivo eliminado correctamente",
|
||||||
|
"message.static_data_refreshed": "Datos estáticos actualizados",
|
||||||
|
|
||||||
|
"error.notification_not_found": "Notificación no encontrada",
|
||||||
|
"error.invalid_platform": "Plataforma no válida",
|
||||||
|
|
||||||
|
"error.upgrade_trigger_not_found": "Trigger de actualización no encontrado",
|
||||||
|
"error.receipt_data_required": "Se requiere receipt_data para iOS",
|
||||||
|
"error.purchase_token_required": "Se requiere purchase_token para Android",
|
||||||
|
|
||||||
|
"error.no_file_provided": "No se proporcionó ningún archivo",
|
||||||
|
|
||||||
|
"error.failed_to_fetch_residence_types": "Error al obtener los tipos de propiedad",
|
||||||
|
"error.failed_to_fetch_task_categories": "Error al obtener las categorías de tareas",
|
||||||
|
"error.failed_to_fetch_task_priorities": "Error al obtener las prioridades de tareas",
|
||||||
|
"error.failed_to_fetch_task_frequencies": "Error al obtener las frecuencias de tareas",
|
||||||
|
"error.failed_to_fetch_task_statuses": "Error al obtener los estados de tareas",
|
||||||
|
"error.failed_to_fetch_contractor_specialties": "Error al obtener las especialidades de contratistas",
|
||||||
|
|
||||||
|
"push.task_due_soon.title": "Tarea Proxima a Vencer",
|
||||||
|
"push.task_due_soon.body": "{{.TaskTitle}} vence {{.DueDate}}",
|
||||||
|
"push.task_overdue.title": "Tarea Vencida",
|
||||||
|
"push.task_overdue.body": "{{.TaskTitle}} esta vencida",
|
||||||
|
"push.task_completed.title": "Tarea Completada",
|
||||||
|
"push.task_completed.body": "{{.UserName}} completo {{.TaskTitle}}",
|
||||||
|
"push.task_assigned.title": "Nueva Tarea Asignada",
|
||||||
|
"push.task_assigned.body": "Se te ha asignado {{.TaskTitle}}",
|
||||||
|
"push.residence_shared.title": "Propiedad Compartida",
|
||||||
|
"push.residence_shared.body": "{{.UserName}} compartio {{.ResidenceName}} contigo",
|
||||||
|
|
||||||
|
"email.welcome.subject": "Bienvenido a Casera!",
|
||||||
|
"email.verification.subject": "Verifica Tu Correo Electronico",
|
||||||
|
"email.password_reset.subject": "Codigo de Restablecimiento de Contrasena",
|
||||||
|
"email.tasks_report.subject": "Informe de Tareas para {{.ResidenceName}}",
|
||||||
|
|
||||||
|
"lookup.residence_type.house": "Casa",
|
||||||
|
"lookup.residence_type.apartment": "Apartamento",
|
||||||
|
"lookup.residence_type.condo": "Condominio",
|
||||||
|
"lookup.residence_type.townhouse": "Casa Adosada",
|
||||||
|
"lookup.residence_type.mobile_home": "Casa Movil",
|
||||||
|
"lookup.residence_type.other": "Otro",
|
||||||
|
|
||||||
|
"lookup.task_category.plumbing": "Plomeria",
|
||||||
|
"lookup.task_category.electrical": "Electricidad",
|
||||||
|
"lookup.task_category.hvac": "Climatizacion",
|
||||||
|
"lookup.task_category.appliances": "Electrodomesticos",
|
||||||
|
"lookup.task_category.exterior": "Exterior",
|
||||||
|
"lookup.task_category.interior": "Interior",
|
||||||
|
"lookup.task_category.landscaping": "Jardineria",
|
||||||
|
"lookup.task_category.safety": "Seguridad",
|
||||||
|
"lookup.task_category.cleaning": "Limpieza",
|
||||||
|
"lookup.task_category.pest_control": "Control de Plagas",
|
||||||
|
"lookup.task_category.seasonal": "Estacional",
|
||||||
|
"lookup.task_category.other": "Otro",
|
||||||
|
|
||||||
|
"lookup.task_priority.low": "Baja",
|
||||||
|
"lookup.task_priority.medium": "Media",
|
||||||
|
"lookup.task_priority.high": "Alta",
|
||||||
|
"lookup.task_priority.urgent": "Urgente",
|
||||||
|
|
||||||
|
"lookup.task_status.pending": "Pendiente",
|
||||||
|
"lookup.task_status.in_progress": "En Progreso",
|
||||||
|
"lookup.task_status.completed": "Completada",
|
||||||
|
"lookup.task_status.cancelled": "Cancelada",
|
||||||
|
"lookup.task_status.archived": "Archivada",
|
||||||
|
|
||||||
|
"lookup.task_frequency.once": "Una Vez",
|
||||||
|
"lookup.task_frequency.daily": "Diario",
|
||||||
|
"lookup.task_frequency.weekly": "Semanal",
|
||||||
|
"lookup.task_frequency.biweekly": "Cada 2 Semanas",
|
||||||
|
"lookup.task_frequency.monthly": "Mensual",
|
||||||
|
"lookup.task_frequency.quarterly": "Trimestral",
|
||||||
|
"lookup.task_frequency.semiannually": "Cada 6 Meses",
|
||||||
|
"lookup.task_frequency.annually": "Anual",
|
||||||
|
|
||||||
|
"lookup.contractor_specialty.plumber": "Plomero",
|
||||||
|
"lookup.contractor_specialty.electrician": "Electricista",
|
||||||
|
"lookup.contractor_specialty.hvac_technician": "Tecnico de Climatizacion",
|
||||||
|
"lookup.contractor_specialty.handyman": "Manitas",
|
||||||
|
"lookup.contractor_specialty.landscaper": "Jardinero",
|
||||||
|
"lookup.contractor_specialty.roofer": "Techador",
|
||||||
|
"lookup.contractor_specialty.painter": "Pintor",
|
||||||
|
"lookup.contractor_specialty.carpenter": "Carpintero",
|
||||||
|
"lookup.contractor_specialty.pest_control": "Control de Plagas",
|
||||||
|
"lookup.contractor_specialty.cleaning": "Limpieza",
|
||||||
|
"lookup.contractor_specialty.pool_service": "Servicio de Piscina",
|
||||||
|
"lookup.contractor_specialty.general_contractor": "Contratista General",
|
||||||
|
"lookup.contractor_specialty.other": "Otro"
|
||||||
|
}
|
||||||
187
internal/i18n/translations/fr.json
Normal file
187
internal/i18n/translations/fr.json
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
{
|
||||||
|
"error.invalid_request_body": "Corps de requete non valide",
|
||||||
|
"error.invalid_credentials": "Identifiants non valides",
|
||||||
|
"error.account_inactive": "Le compte est inactif",
|
||||||
|
"error.username_taken": "Nom d'utilisateur deja pris",
|
||||||
|
"error.email_taken": "Email deja enregistre",
|
||||||
|
"error.email_already_taken": "Email deja utilise",
|
||||||
|
"error.registration_failed": "Echec de l'inscription",
|
||||||
|
"error.not_authenticated": "Non authentifie",
|
||||||
|
"error.failed_to_get_user": "Echec de la recuperation de l'utilisateur",
|
||||||
|
"error.failed_to_update_profile": "Echec de la mise a jour du profil",
|
||||||
|
"error.invalid_verification_code": "Code de verification non valide",
|
||||||
|
"error.verification_code_expired": "Le code de verification a expire",
|
||||||
|
"error.email_already_verified": "Email deja verifie",
|
||||||
|
"error.verification_failed": "Echec de la verification",
|
||||||
|
"error.failed_to_resend_verification": "Echec du renvoi de la verification",
|
||||||
|
"error.rate_limit_exceeded": "Trop de demandes de reinitialisation de mot de passe. Veuillez reessayer plus tard.",
|
||||||
|
"error.too_many_attempts": "Trop de tentatives. Veuillez demander un nouveau code.",
|
||||||
|
"error.invalid_reset_token": "Jeton de reinitialisation non valide ou expire",
|
||||||
|
"error.password_reset_failed": "Echec de la reinitialisation du mot de passe",
|
||||||
|
"error.apple_signin_not_configured": "La connexion Apple n'est pas configuree",
|
||||||
|
"error.apple_signin_failed": "Echec de la connexion Apple",
|
||||||
|
"error.invalid_apple_token": "Jeton d'identite Apple non valide",
|
||||||
|
|
||||||
|
"error.invalid_task_id": "ID de tache non valide",
|
||||||
|
"error.invalid_residence_id": "ID de propriete non valide",
|
||||||
|
"error.invalid_contractor_id": "ID de prestataire non valide",
|
||||||
|
"error.invalid_document_id": "ID de document non valide",
|
||||||
|
"error.invalid_completion_id": "ID de completion non valide",
|
||||||
|
"error.invalid_user_id": "ID d'utilisateur non valide",
|
||||||
|
"error.invalid_notification_id": "ID de notification non valide",
|
||||||
|
"error.invalid_device_id": "ID d'appareil non valide",
|
||||||
|
|
||||||
|
"error.task_not_found": "Tache non trouvee",
|
||||||
|
"error.residence_not_found": "Propriete non trouvee",
|
||||||
|
"error.contractor_not_found": "Prestataire non trouve",
|
||||||
|
"error.document_not_found": "Document non trouve",
|
||||||
|
"error.completion_not_found": "Completion de tache non trouvee",
|
||||||
|
"error.user_not_found": "Utilisateur non trouve",
|
||||||
|
"error.share_code_invalid": "Code de partage non valide",
|
||||||
|
"error.share_code_expired": "Le code de partage a expire",
|
||||||
|
|
||||||
|
"error.task_access_denied": "Vous n'avez pas acces a cette tache",
|
||||||
|
"error.residence_access_denied": "Vous n'avez pas acces a cette propriete",
|
||||||
|
"error.contractor_access_denied": "Vous n'avez pas acces a ce prestataire",
|
||||||
|
"error.document_access_denied": "Vous n'avez pas acces a ce document",
|
||||||
|
"error.not_residence_owner": "Seul le proprietaire peut effectuer cette action",
|
||||||
|
"error.cannot_remove_owner": "Impossible de retirer le proprietaire",
|
||||||
|
"error.user_already_member": "L'utilisateur est deja membre de cette propriete",
|
||||||
|
"error.properties_limit_reached": "Vous avez atteint le nombre maximum de proprietes pour votre abonnement",
|
||||||
|
|
||||||
|
"error.task_already_cancelled": "La tache est deja annulee",
|
||||||
|
"error.task_already_archived": "La tache est deja archivee",
|
||||||
|
|
||||||
|
"error.failed_to_parse_form": "Echec de l'analyse du formulaire",
|
||||||
|
"error.task_id_required": "task_id est requis",
|
||||||
|
"error.invalid_task_id_value": "task_id non valide",
|
||||||
|
"error.failed_to_upload_image": "Echec du telechargement de l'image",
|
||||||
|
"error.residence_id_required": "residence_id est requis",
|
||||||
|
"error.invalid_residence_id_value": "residence_id non valide",
|
||||||
|
"error.title_required": "Le titre est requis",
|
||||||
|
"error.failed_to_upload_file": "Echec du telechargement du fichier",
|
||||||
|
|
||||||
|
"message.logged_out": "Deconnexion reussie",
|
||||||
|
"message.email_verified": "Email verifie avec succes",
|
||||||
|
"message.verification_email_sent": "Email de verification envoye",
|
||||||
|
"message.password_reset_email_sent": "Si un compte existe avec cet email, un code de reinitialisation a ete envoye.",
|
||||||
|
"message.reset_code_verified": "Code verifie avec succes",
|
||||||
|
"message.password_reset_success": "Mot de passe reinitialise avec succes. Veuillez vous connecter avec votre nouveau mot de passe.",
|
||||||
|
|
||||||
|
"message.task_deleted": "Tache supprimee avec succes",
|
||||||
|
"message.task_in_progress": "Tache marquee comme en cours",
|
||||||
|
"message.task_cancelled": "Tache annulee",
|
||||||
|
"message.task_uncancelled": "Tache reactived",
|
||||||
|
"message.task_archived": "Tache archivee",
|
||||||
|
"message.task_unarchived": "Tache desarchivee",
|
||||||
|
"message.completion_deleted": "Completion supprimee avec succes",
|
||||||
|
|
||||||
|
"message.residence_deleted": "Propriete supprimee avec succes",
|
||||||
|
"message.user_removed": "Utilisateur retire de la propriete",
|
||||||
|
"message.tasks_report_generated": "Rapport de taches genere avec succes",
|
||||||
|
"message.tasks_report_sent": "Rapport de taches genere et envoye a {{.Email}}",
|
||||||
|
"message.tasks_report_email_failed": "Rapport de taches genere mais l'email n'a pas pu etre envoye",
|
||||||
|
|
||||||
|
"message.contractor_deleted": "Prestataire supprime avec succes",
|
||||||
|
|
||||||
|
"message.document_deleted": "Document supprime avec succes",
|
||||||
|
"message.document_activated": "Document active",
|
||||||
|
"message.document_deactivated": "Document desactive",
|
||||||
|
|
||||||
|
"message.notification_marked_read": "Notification marquée comme lue",
|
||||||
|
"message.all_notifications_marked_read": "Toutes les notifications marquées comme lues",
|
||||||
|
"message.device_removed": "Appareil supprimé",
|
||||||
|
|
||||||
|
"message.subscription_upgraded": "Abonnement mis à niveau avec succès",
|
||||||
|
"message.subscription_cancelled": "Abonnement annulé. Vous conserverez les avantages Pro jusqu'à la fin de votre période de facturation.",
|
||||||
|
"message.subscription_restored": "Abonnement restauré avec succès",
|
||||||
|
|
||||||
|
"message.file_deleted": "Fichier supprimé avec succès",
|
||||||
|
"message.static_data_refreshed": "Données statiques actualisées",
|
||||||
|
|
||||||
|
"error.notification_not_found": "Notification non trouvée",
|
||||||
|
"error.invalid_platform": "Plateforme non valide",
|
||||||
|
|
||||||
|
"error.upgrade_trigger_not_found": "Déclencheur de mise à niveau non trouvé",
|
||||||
|
"error.receipt_data_required": "receipt_data est requis pour iOS",
|
||||||
|
"error.purchase_token_required": "purchase_token est requis pour Android",
|
||||||
|
|
||||||
|
"error.no_file_provided": "Aucun fichier fourni",
|
||||||
|
|
||||||
|
"error.failed_to_fetch_residence_types": "Échec de la récupération des types de propriété",
|
||||||
|
"error.failed_to_fetch_task_categories": "Échec de la récupération des catégories de tâches",
|
||||||
|
"error.failed_to_fetch_task_priorities": "Échec de la récupération des priorités de tâches",
|
||||||
|
"error.failed_to_fetch_task_frequencies": "Échec de la récupération des fréquences de tâches",
|
||||||
|
"error.failed_to_fetch_task_statuses": "Échec de la récupération des statuts de tâches",
|
||||||
|
"error.failed_to_fetch_contractor_specialties": "Échec de la récupération des spécialités des prestataires",
|
||||||
|
|
||||||
|
"push.task_due_soon.title": "Tache Bientot Due",
|
||||||
|
"push.task_due_soon.body": "{{.TaskTitle}} est due le {{.DueDate}}",
|
||||||
|
"push.task_overdue.title": "Tache en Retard",
|
||||||
|
"push.task_overdue.body": "{{.TaskTitle}} est en retard",
|
||||||
|
"push.task_completed.title": "Tache Terminee",
|
||||||
|
"push.task_completed.body": "{{.UserName}} a termine {{.TaskTitle}}",
|
||||||
|
"push.task_assigned.title": "Nouvelle Tache Assignee",
|
||||||
|
"push.task_assigned.body": "{{.TaskTitle}} vous a ete assignee",
|
||||||
|
"push.residence_shared.title": "Propriete Partagee",
|
||||||
|
"push.residence_shared.body": "{{.UserName}} a partage {{.ResidenceName}} avec vous",
|
||||||
|
|
||||||
|
"email.welcome.subject": "Bienvenue sur Casera !",
|
||||||
|
"email.verification.subject": "Verifiez Votre Email",
|
||||||
|
"email.password_reset.subject": "Code de Reinitialisation de Mot de Passe",
|
||||||
|
"email.tasks_report.subject": "Rapport de Taches pour {{.ResidenceName}}",
|
||||||
|
|
||||||
|
"lookup.residence_type.house": "Maison",
|
||||||
|
"lookup.residence_type.apartment": "Appartement",
|
||||||
|
"lookup.residence_type.condo": "Copropriete",
|
||||||
|
"lookup.residence_type.townhouse": "Maison de Ville",
|
||||||
|
"lookup.residence_type.mobile_home": "Mobil-home",
|
||||||
|
"lookup.residence_type.other": "Autre",
|
||||||
|
|
||||||
|
"lookup.task_category.plumbing": "Plomberie",
|
||||||
|
"lookup.task_category.electrical": "Electricite",
|
||||||
|
"lookup.task_category.hvac": "Climatisation",
|
||||||
|
"lookup.task_category.appliances": "Electromenager",
|
||||||
|
"lookup.task_category.exterior": "Exterieur",
|
||||||
|
"lookup.task_category.interior": "Interieur",
|
||||||
|
"lookup.task_category.landscaping": "Jardinage",
|
||||||
|
"lookup.task_category.safety": "Securite",
|
||||||
|
"lookup.task_category.cleaning": "Nettoyage",
|
||||||
|
"lookup.task_category.pest_control": "Lutte Antiparasitaire",
|
||||||
|
"lookup.task_category.seasonal": "Saisonnier",
|
||||||
|
"lookup.task_category.other": "Autre",
|
||||||
|
|
||||||
|
"lookup.task_priority.low": "Basse",
|
||||||
|
"lookup.task_priority.medium": "Moyenne",
|
||||||
|
"lookup.task_priority.high": "Haute",
|
||||||
|
"lookup.task_priority.urgent": "Urgente",
|
||||||
|
|
||||||
|
"lookup.task_status.pending": "En Attente",
|
||||||
|
"lookup.task_status.in_progress": "En Cours",
|
||||||
|
"lookup.task_status.completed": "Terminee",
|
||||||
|
"lookup.task_status.cancelled": "Annulee",
|
||||||
|
"lookup.task_status.archived": "Archivee",
|
||||||
|
|
||||||
|
"lookup.task_frequency.once": "Une Fois",
|
||||||
|
"lookup.task_frequency.daily": "Quotidien",
|
||||||
|
"lookup.task_frequency.weekly": "Hebdomadaire",
|
||||||
|
"lookup.task_frequency.biweekly": "Toutes les 2 Semaines",
|
||||||
|
"lookup.task_frequency.monthly": "Mensuel",
|
||||||
|
"lookup.task_frequency.quarterly": "Trimestriel",
|
||||||
|
"lookup.task_frequency.semiannually": "Tous les 6 Mois",
|
||||||
|
"lookup.task_frequency.annually": "Annuel",
|
||||||
|
|
||||||
|
"lookup.contractor_specialty.plumber": "Plombier",
|
||||||
|
"lookup.contractor_specialty.electrician": "Electricien",
|
||||||
|
"lookup.contractor_specialty.hvac_technician": "Technicien CVC",
|
||||||
|
"lookup.contractor_specialty.handyman": "Bricoleur",
|
||||||
|
"lookup.contractor_specialty.landscaper": "Paysagiste",
|
||||||
|
"lookup.contractor_specialty.roofer": "Couvreur",
|
||||||
|
"lookup.contractor_specialty.painter": "Peintre",
|
||||||
|
"lookup.contractor_specialty.carpenter": "Menuisier",
|
||||||
|
"lookup.contractor_specialty.pest_control": "Desinsectisation",
|
||||||
|
"lookup.contractor_specialty.cleaning": "Nettoyage",
|
||||||
|
"lookup.contractor_specialty.pool_service": "Service Piscine",
|
||||||
|
"lookup.contractor_specialty.general_contractor": "Entrepreneur General",
|
||||||
|
"lookup.contractor_specialty.other": "Autre"
|
||||||
|
}
|
||||||
187
internal/i18n/translations/pt.json
Normal file
187
internal/i18n/translations/pt.json
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
{
|
||||||
|
"error.invalid_request_body": "Corpo da solicitacao invalido",
|
||||||
|
"error.invalid_credentials": "Credenciais invalidas",
|
||||||
|
"error.account_inactive": "A conta esta inativa",
|
||||||
|
"error.username_taken": "Nome de usuario ja em uso",
|
||||||
|
"error.email_taken": "Email ja registrado",
|
||||||
|
"error.email_already_taken": "Email ja em uso",
|
||||||
|
"error.registration_failed": "Falha no registro",
|
||||||
|
"error.not_authenticated": "Nao autenticado",
|
||||||
|
"error.failed_to_get_user": "Falha ao obter usuario",
|
||||||
|
"error.failed_to_update_profile": "Falha ao atualizar perfil",
|
||||||
|
"error.invalid_verification_code": "Codigo de verificacao invalido",
|
||||||
|
"error.verification_code_expired": "O codigo de verificacao expirou",
|
||||||
|
"error.email_already_verified": "Email ja verificado",
|
||||||
|
"error.verification_failed": "Falha na verificacao",
|
||||||
|
"error.failed_to_resend_verification": "Falha ao reenviar verificacao",
|
||||||
|
"error.rate_limit_exceeded": "Muitas solicitacoes de redefinicao de senha. Por favor, tente novamente mais tarde.",
|
||||||
|
"error.too_many_attempts": "Muitas tentativas. Por favor, solicite um novo codigo.",
|
||||||
|
"error.invalid_reset_token": "Token de redefinicao invalido ou expirado",
|
||||||
|
"error.password_reset_failed": "Falha na redefinicao de senha",
|
||||||
|
"error.apple_signin_not_configured": "O login com Apple nao esta configurado",
|
||||||
|
"error.apple_signin_failed": "Falha no login com Apple",
|
||||||
|
"error.invalid_apple_token": "Token de identidade Apple invalido",
|
||||||
|
|
||||||
|
"error.invalid_task_id": "ID da tarefa invalido",
|
||||||
|
"error.invalid_residence_id": "ID da propriedade invalido",
|
||||||
|
"error.invalid_contractor_id": "ID do prestador invalido",
|
||||||
|
"error.invalid_document_id": "ID do documento invalido",
|
||||||
|
"error.invalid_completion_id": "ID de conclusao invalido",
|
||||||
|
"error.invalid_user_id": "ID do usuario invalido",
|
||||||
|
"error.invalid_notification_id": "ID da notificacao invalido",
|
||||||
|
"error.invalid_device_id": "ID do dispositivo invalido",
|
||||||
|
|
||||||
|
"error.task_not_found": "Tarefa nao encontrada",
|
||||||
|
"error.residence_not_found": "Propriedade nao encontrada",
|
||||||
|
"error.contractor_not_found": "Prestador nao encontrado",
|
||||||
|
"error.document_not_found": "Documento nao encontrado",
|
||||||
|
"error.completion_not_found": "Conclusao da tarefa nao encontrada",
|
||||||
|
"error.user_not_found": "Usuario nao encontrado",
|
||||||
|
"error.share_code_invalid": "Codigo de compartilhamento invalido",
|
||||||
|
"error.share_code_expired": "O codigo de compartilhamento expirou",
|
||||||
|
|
||||||
|
"error.task_access_denied": "Voce nao tem acesso a esta tarefa",
|
||||||
|
"error.residence_access_denied": "Voce nao tem acesso a esta propriedade",
|
||||||
|
"error.contractor_access_denied": "Voce nao tem acesso a este prestador",
|
||||||
|
"error.document_access_denied": "Voce nao tem acesso a este documento",
|
||||||
|
"error.not_residence_owner": "Apenas o proprietario pode realizar esta acao",
|
||||||
|
"error.cannot_remove_owner": "Nao e possivel remover o proprietario",
|
||||||
|
"error.user_already_member": "O usuario ja e membro desta propriedade",
|
||||||
|
"error.properties_limit_reached": "Voce atingiu o numero maximo de propriedades para sua assinatura",
|
||||||
|
|
||||||
|
"error.task_already_cancelled": "A tarefa ja esta cancelada",
|
||||||
|
"error.task_already_archived": "A tarefa ja esta arquivada",
|
||||||
|
|
||||||
|
"error.failed_to_parse_form": "Falha ao analisar o formulario",
|
||||||
|
"error.task_id_required": "task_id e obrigatorio",
|
||||||
|
"error.invalid_task_id_value": "task_id invalido",
|
||||||
|
"error.failed_to_upload_image": "Falha ao enviar imagem",
|
||||||
|
"error.residence_id_required": "residence_id e obrigatorio",
|
||||||
|
"error.invalid_residence_id_value": "residence_id invalido",
|
||||||
|
"error.title_required": "Titulo e obrigatorio",
|
||||||
|
"error.failed_to_upload_file": "Falha ao enviar arquivo",
|
||||||
|
|
||||||
|
"message.logged_out": "Logout realizado com sucesso",
|
||||||
|
"message.email_verified": "Email verificado com sucesso",
|
||||||
|
"message.verification_email_sent": "Email de verificacao enviado",
|
||||||
|
"message.password_reset_email_sent": "Se existir uma conta com este email, um codigo de redefinicao foi enviado.",
|
||||||
|
"message.reset_code_verified": "Codigo verificado com sucesso",
|
||||||
|
"message.password_reset_success": "Senha redefinida com sucesso. Por favor, faca login com sua nova senha.",
|
||||||
|
|
||||||
|
"message.task_deleted": "Tarefa excluida com sucesso",
|
||||||
|
"message.task_in_progress": "Tarefa marcada como em andamento",
|
||||||
|
"message.task_cancelled": "Tarefa cancelada",
|
||||||
|
"message.task_uncancelled": "Tarefa reativada",
|
||||||
|
"message.task_archived": "Tarefa arquivada",
|
||||||
|
"message.task_unarchived": "Tarefa desarquivada",
|
||||||
|
"message.completion_deleted": "Conclusao excluida com sucesso",
|
||||||
|
|
||||||
|
"message.residence_deleted": "Propriedade excluida com sucesso",
|
||||||
|
"message.user_removed": "Usuario removido da propriedade",
|
||||||
|
"message.tasks_report_generated": "Relatorio de tarefas gerado com sucesso",
|
||||||
|
"message.tasks_report_sent": "Relatorio de tarefas gerado e enviado para {{.Email}}",
|
||||||
|
"message.tasks_report_email_failed": "Relatorio de tarefas gerado mas o email nao pode ser enviado",
|
||||||
|
|
||||||
|
"message.contractor_deleted": "Prestador excluido com sucesso",
|
||||||
|
|
||||||
|
"message.document_deleted": "Documento excluido com sucesso",
|
||||||
|
"message.document_activated": "Documento ativado",
|
||||||
|
"message.document_deactivated": "Documento desativado",
|
||||||
|
|
||||||
|
"message.notification_marked_read": "Notificação marcada como lida",
|
||||||
|
"message.all_notifications_marked_read": "Todas as notificações marcadas como lidas",
|
||||||
|
"message.device_removed": "Dispositivo removido",
|
||||||
|
|
||||||
|
"message.subscription_upgraded": "Assinatura atualizada com sucesso",
|
||||||
|
"message.subscription_cancelled": "Assinatura cancelada. Você manterá os benefícios Pro até o final do seu período de faturamento.",
|
||||||
|
"message.subscription_restored": "Assinatura restaurada com sucesso",
|
||||||
|
|
||||||
|
"message.file_deleted": "Arquivo excluído com sucesso",
|
||||||
|
"message.static_data_refreshed": "Dados estáticos atualizados",
|
||||||
|
|
||||||
|
"error.notification_not_found": "Notificação não encontrada",
|
||||||
|
"error.invalid_platform": "Plataforma inválida",
|
||||||
|
|
||||||
|
"error.upgrade_trigger_not_found": "Gatilho de atualização não encontrado",
|
||||||
|
"error.receipt_data_required": "receipt_data é obrigatório para iOS",
|
||||||
|
"error.purchase_token_required": "purchase_token é obrigatório para Android",
|
||||||
|
|
||||||
|
"error.no_file_provided": "Nenhum arquivo fornecido",
|
||||||
|
|
||||||
|
"error.failed_to_fetch_residence_types": "Falha ao buscar tipos de propriedade",
|
||||||
|
"error.failed_to_fetch_task_categories": "Falha ao buscar categorias de tarefas",
|
||||||
|
"error.failed_to_fetch_task_priorities": "Falha ao buscar prioridades de tarefas",
|
||||||
|
"error.failed_to_fetch_task_frequencies": "Falha ao buscar frequências de tarefas",
|
||||||
|
"error.failed_to_fetch_task_statuses": "Falha ao buscar status de tarefas",
|
||||||
|
"error.failed_to_fetch_contractor_specialties": "Falha ao buscar especialidades de prestadores",
|
||||||
|
|
||||||
|
"push.task_due_soon.title": "Tarefa Proxima do Vencimento",
|
||||||
|
"push.task_due_soon.body": "{{.TaskTitle}} vence em {{.DueDate}}",
|
||||||
|
"push.task_overdue.title": "Tarefa Atrasada",
|
||||||
|
"push.task_overdue.body": "{{.TaskTitle}} esta atrasada",
|
||||||
|
"push.task_completed.title": "Tarefa Concluida",
|
||||||
|
"push.task_completed.body": "{{.UserName}} concluiu {{.TaskTitle}}",
|
||||||
|
"push.task_assigned.title": "Nova Tarefa Atribuida",
|
||||||
|
"push.task_assigned.body": "{{.TaskTitle}} foi atribuida a voce",
|
||||||
|
"push.residence_shared.title": "Propriedade Compartilhada",
|
||||||
|
"push.residence_shared.body": "{{.UserName}} compartilhou {{.ResidenceName}} com voce",
|
||||||
|
|
||||||
|
"email.welcome.subject": "Bem-vindo ao Casera!",
|
||||||
|
"email.verification.subject": "Verifique Seu Email",
|
||||||
|
"email.password_reset.subject": "Codigo de Redefinicao de Senha",
|
||||||
|
"email.tasks_report.subject": "Relatorio de Tarefas para {{.ResidenceName}}",
|
||||||
|
|
||||||
|
"lookup.residence_type.house": "Casa",
|
||||||
|
"lookup.residence_type.apartment": "Apartamento",
|
||||||
|
"lookup.residence_type.condo": "Condominio",
|
||||||
|
"lookup.residence_type.townhouse": "Sobrado",
|
||||||
|
"lookup.residence_type.mobile_home": "Casa Movel",
|
||||||
|
"lookup.residence_type.other": "Outro",
|
||||||
|
|
||||||
|
"lookup.task_category.plumbing": "Encanamento",
|
||||||
|
"lookup.task_category.electrical": "Eletrica",
|
||||||
|
"lookup.task_category.hvac": "Climatizacao",
|
||||||
|
"lookup.task_category.appliances": "Eletrodomesticos",
|
||||||
|
"lookup.task_category.exterior": "Exterior",
|
||||||
|
"lookup.task_category.interior": "Interior",
|
||||||
|
"lookup.task_category.landscaping": "Paisagismo",
|
||||||
|
"lookup.task_category.safety": "Seguranca",
|
||||||
|
"lookup.task_category.cleaning": "Limpeza",
|
||||||
|
"lookup.task_category.pest_control": "Controle de Pragas",
|
||||||
|
"lookup.task_category.seasonal": "Sazonal",
|
||||||
|
"lookup.task_category.other": "Outro",
|
||||||
|
|
||||||
|
"lookup.task_priority.low": "Baixa",
|
||||||
|
"lookup.task_priority.medium": "Media",
|
||||||
|
"lookup.task_priority.high": "Alta",
|
||||||
|
"lookup.task_priority.urgent": "Urgente",
|
||||||
|
|
||||||
|
"lookup.task_status.pending": "Pendente",
|
||||||
|
"lookup.task_status.in_progress": "Em Andamento",
|
||||||
|
"lookup.task_status.completed": "Concluida",
|
||||||
|
"lookup.task_status.cancelled": "Cancelada",
|
||||||
|
"lookup.task_status.archived": "Arquivada",
|
||||||
|
|
||||||
|
"lookup.task_frequency.once": "Uma Vez",
|
||||||
|
"lookup.task_frequency.daily": "Diario",
|
||||||
|
"lookup.task_frequency.weekly": "Semanal",
|
||||||
|
"lookup.task_frequency.biweekly": "Quinzenal",
|
||||||
|
"lookup.task_frequency.monthly": "Mensal",
|
||||||
|
"lookup.task_frequency.quarterly": "Trimestral",
|
||||||
|
"lookup.task_frequency.semiannually": "Semestral",
|
||||||
|
"lookup.task_frequency.annually": "Anual",
|
||||||
|
|
||||||
|
"lookup.contractor_specialty.plumber": "Encanador",
|
||||||
|
"lookup.contractor_specialty.electrician": "Eletricista",
|
||||||
|
"lookup.contractor_specialty.hvac_technician": "Tecnico de Climatizacao",
|
||||||
|
"lookup.contractor_specialty.handyman": "Faz-tudo",
|
||||||
|
"lookup.contractor_specialty.landscaper": "Paisagista",
|
||||||
|
"lookup.contractor_specialty.roofer": "Telhadista",
|
||||||
|
"lookup.contractor_specialty.painter": "Pintor",
|
||||||
|
"lookup.contractor_specialty.carpenter": "Carpinteiro",
|
||||||
|
"lookup.contractor_specialty.pest_control": "Controle de Pragas",
|
||||||
|
"lookup.contractor_specialty.cleaning": "Limpeza",
|
||||||
|
"lookup.contractor_specialty.pool_service": "Servico de Piscina",
|
||||||
|
"lookup.contractor_specialty.general_contractor": "Empreiteiro Geral",
|
||||||
|
"lookup.contractor_specialty.other": "Outro"
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/treytartt/casera-api/internal/admin"
|
"github.com/treytartt/casera-api/internal/admin"
|
||||||
"github.com/treytartt/casera-api/internal/config"
|
"github.com/treytartt/casera-api/internal/config"
|
||||||
"github.com/treytartt/casera-api/internal/handlers"
|
"github.com/treytartt/casera-api/internal/handlers"
|
||||||
|
"github.com/treytartt/casera-api/internal/i18n"
|
||||||
"github.com/treytartt/casera-api/internal/middleware"
|
"github.com/treytartt/casera-api/internal/middleware"
|
||||||
"github.com/treytartt/casera-api/internal/push"
|
"github.com/treytartt/casera-api/internal/push"
|
||||||
"github.com/treytartt/casera-api/internal/repositories"
|
"github.com/treytartt/casera-api/internal/repositories"
|
||||||
@@ -48,6 +49,7 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
|
|||||||
r.Use(utils.GinRecovery())
|
r.Use(utils.GinRecovery())
|
||||||
r.Use(utils.GinLogger())
|
r.Use(utils.GinLogger())
|
||||||
r.Use(corsMiddleware(cfg))
|
r.Use(corsMiddleware(cfg))
|
||||||
|
r.Use(i18n.Middleware())
|
||||||
|
|
||||||
// Health check endpoint (no auth required)
|
// Health check endpoint (no auth required)
|
||||||
r.GET("/api/health/", healthCheck)
|
r.GET("/api/health/", healthCheck)
|
||||||
|
|||||||
Reference in New Issue
Block a user