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:
Trey t
2025-12-02 02:01:47 -06:00
parent c72741fd5f
commit c17e85c14e
22 changed files with 1771 additions and 193 deletions

View File

@@ -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).

View 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
View File

@@ -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
View File

@@ -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=

View File

@@ -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")

View File

@@ -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()})

View File

@@ -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})
} }

View File

@@ -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")})
} }

View File

@@ -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{

View File

@@ -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",
}) })
} }

View File

@@ -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,
}) })
} }

View File

@@ -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 ===

View File

@@ -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")})
} }

View File

@@ -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
View 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
View 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)
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View File

@@ -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)