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

@@ -9,6 +9,7 @@ import (
"github.com/treytartt/casera-api/internal/dto/requests"
"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/services"
)
@@ -40,7 +41,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
var req requests.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
Error: "Invalid request body",
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
Details: map[string]string{
"validation": err.Error(),
},
@@ -51,10 +52,10 @@ func (h *AuthHandler) Login(c *gin.Context) {
response, err := h.authService.Login(&req)
if err != nil {
status := http.StatusUnauthorized
message := "Invalid credentials"
message := i18n.LocalizedMessage(c, "error.invalid_credentials")
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")
@@ -70,7 +71,7 @@ func (h *AuthHandler) Register(c *gin.Context) {
var req requests.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
Error: "Invalid request body",
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
Details: map[string]string{
"validation": err.Error(),
},
@@ -84,12 +85,12 @@ func (h *AuthHandler) Register(c *gin.Context) {
message := err.Error()
if errors.Is(err, services.ErrUsernameTaken) {
message = "Username already taken"
message = i18n.LocalizedMessage(c, "error.username_taken")
} else if errors.Is(err, services.ErrEmailTaken) {
message = "Email already registered"
message = i18n.LocalizedMessage(c, "error.email_taken")
} else {
status = http.StatusInternalServerError
message = "Registration failed"
message = i18n.LocalizedMessage(c, "error.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) {
token := middleware.GetAuthToken(c)
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
}
@@ -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/
@@ -142,7 +143,7 @@ func (h *AuthHandler) CurrentUser(c *gin.Context) {
response, err := h.authService.GetCurrentUser(user.ID)
if err != nil {
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
}
@@ -159,7 +160,7 @@ func (h *AuthHandler) UpdateProfile(c *gin.Context) {
var req requests.UpdateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
Error: "Invalid request body",
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
Details: map[string]string{
"validation": err.Error(),
},
@@ -170,12 +171,12 @@ func (h *AuthHandler) UpdateProfile(c *gin.Context) {
response, err := h.authService.UpdateProfile(user.ID, &req)
if err != nil {
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
}
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
}
@@ -192,7 +193,7 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
var req requests.VerifyEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
Error: "Invalid request body",
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
Details: map[string]string{
"validation": err.Error(),
},
@@ -206,14 +207,14 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
message := err.Error()
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) {
message = "Verification code has expired"
message = i18n.LocalizedMessage(c, "error.verification_code_expired")
} else if errors.Is(err, services.ErrAlreadyVerified) {
message = "Email already verified"
message = i18n.LocalizedMessage(c, "error.email_already_verified")
} else {
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")
}
@@ -222,7 +223,7 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
}
c.JSON(http.StatusOK, responses.VerifyEmailResponse{
Message: "Email verified successfully",
Message: i18n.LocalizedMessage(c, "message.email_verified"),
Verified: true,
})
}
@@ -237,12 +238,12 @@ func (h *AuthHandler) ResendVerification(c *gin.Context) {
code, err := h.authService.ResendVerificationCode(user.ID)
if err != nil {
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
}
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
}
@@ -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/
@@ -263,7 +264,7 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
var req requests.ForgotPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
Error: "Invalid request body",
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
Details: map[string]string{
"validation": err.Error(),
},
@@ -275,7 +276,7 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
if err != nil {
if errors.Is(err, services.ErrRateLimitExceeded) {
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
}
@@ -295,7 +296,7 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
// Always return success to prevent email enumeration
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
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
Error: "Invalid request body",
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
Details: map[string]string{
"validation": err.Error(),
},
@@ -315,13 +316,13 @@ func (h *AuthHandler) VerifyResetCode(c *gin.Context) {
resetToken, err := h.authService.VerifyResetCode(req.Email, req.Code)
if err != nil {
status := http.StatusBadRequest
message := "Invalid verification code"
message := i18n.LocalizedMessage(c, "error.invalid_verification_code")
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) {
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})
@@ -329,7 +330,7 @@ func (h *AuthHandler) VerifyResetCode(c *gin.Context) {
}
c.JSON(http.StatusOK, responses.VerifyResetCodeResponse{
Message: "Code verified successfully",
Message: i18n.LocalizedMessage(c, "message.reset_code_verified"),
ResetToken: resetToken,
})
}
@@ -339,7 +340,7 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
var req requests.ResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
Error: "Invalid request body",
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
Details: map[string]string{
"validation": err.Error(),
},
@@ -350,13 +351,13 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
err := h.authService.ResetPassword(req.ResetToken, req.NewPassword)
if err != nil {
status := http.StatusBadRequest
message := "Invalid or expired reset token"
message := i18n.LocalizedMessage(c, "error.invalid_reset_token")
if errors.Is(err, services.ErrInvalidResetToken) {
message = "Invalid or expired reset token"
message = i18n.LocalizedMessage(c, "error.invalid_reset_token")
} else {
status = http.StatusInternalServerError
message = "Password reset failed"
message = i18n.LocalizedMessage(c, "error.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{
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
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
Error: "Invalid request body",
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
Details: map[string]string{
"validation": err.Error(),
},
@@ -385,7 +386,7 @@ func (h *AuthHandler) AppleSignIn(c *gin.Context) {
if h.appleAuthService == nil {
log.Error().Msg("Apple auth service not configured")
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{
Error: "Apple Sign In is not configured",
Error: i18n.LocalizedMessage(c, "error.apple_signin_not_configured"),
})
return
}
@@ -393,12 +394,12 @@ func (h *AuthHandler) AppleSignIn(c *gin.Context) {
response, err := h.authService.AppleSignIn(c.Request.Context(), h.appleAuthService, &req)
if err != nil {
status := http.StatusUnauthorized
message := "Apple Sign In failed"
message := i18n.LocalizedMessage(c, "error.apple_signin_failed")
if errors.Is(err, services.ErrUserInactive) {
message = "Account is inactive"
message = i18n.LocalizedMessage(c, "error.account_inactive")
} 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")

View File

@@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin"
"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/models"
"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)
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -47,9 +48,9 @@ func (h *ContractorHandler) GetContractor(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
default:
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)
if err != nil {
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
}
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)
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -98,9 +99,9 @@ func (h *ContractorHandler) UpdateContractor(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
default:
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)
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -122,15 +123,15 @@ func (h *ContractorHandler) DeleteContractor(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -138,7 +139,7 @@ func (h *ContractorHandler) ToggleFavorite(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -146,9 +147,9 @@ func (h *ContractorHandler) ToggleFavorite(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
default:
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)
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -170,9 +171,9 @@ func (h *ContractorHandler) GetContractorTasks(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
default:
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)
residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32)
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
}
response, err := h.contractorService.ListContractorsByResidence(uint(residenceID), user.ID)
if err != nil {
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
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

View File

@@ -12,6 +12,7 @@ import (
"github.com/shopspring/decimal"
"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/models"
"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)
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -55,9 +56,9 @@ func (h *DocumentHandler) GetDocument(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
default:
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") {
// Parse multipart form
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
}
// Parse residence_id (required)
residenceIDStr := c.PostForm("residence_id")
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
}
residenceID, err := strconv.ParseUint(residenceIDStr, 10, 32)
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
}
req.ResidenceID = uint(residenceID)
@@ -109,7 +110,7 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
// Parse title (required)
req.Title = c.PostForm("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
}
@@ -170,7 +171,7 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
if uploadedFile != nil && h.storageService != nil {
result, err := h.storageService.Upload(uploadedFile, "documents")
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
}
req.FileURL = result.URL
@@ -190,7 +191,7 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
response, err := h.documentService.CreateDocument(&req, user.ID)
if err != nil {
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
}
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)
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -218,9 +219,9 @@ func (h *DocumentHandler) UpdateDocument(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
default:
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)
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -242,15 +243,15 @@ func (h *DocumentHandler) DeleteDocument(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -258,7 +259,7 @@ func (h *DocumentHandler) ActivateDocument(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -266,15 +267,15 @@ func (h *DocumentHandler) ActivateDocument(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -282,7 +283,7 @@ func (h *DocumentHandler) DeactivateDocument(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -290,13 +291,13 @@ func (h *DocumentHandler) DeactivateDocument(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/treytartt/casera-api/internal/i18n"
"github.com/treytartt/casera-api/internal/middleware"
"github.com/treytartt/casera-api/internal/models"
"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)
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
}
err = h.notificationService.MarkAsRead(uint(notificationID), user.ID)
if err != nil {
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
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
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/
@@ -97,7 +98,7 @@ func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) {
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/
@@ -145,7 +146,7 @@ func (h *NotificationHandler) RegisterDevice(c *gin.Context) {
device, err := h.notificationService.RegisterDevice(user.ID, &req)
if err != nil {
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
}
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)
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
}
@@ -186,12 +187,12 @@ func (h *NotificationHandler) DeleteDevice(c *gin.Context) {
err = h.notificationService.DeleteDevice(uint(deviceID), platform, user.ID)
if err != nil {
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
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
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/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/models"
"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)
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
}
@@ -69,9 +70,9 @@ func (h *ResidenceHandler) GetResidence(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
default:
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)
if err != nil {
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
}
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)
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
}
@@ -124,9 +125,9 @@ func (h *ResidenceHandler) UpdateResidence(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
default:
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)
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
}
@@ -150,16 +151,16 @@ func (h *ResidenceHandler) DeleteResidence(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -168,7 +169,7 @@ func (h *ResidenceHandler) GenerateShareCode(c *gin.Context) {
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -180,9 +181,9 @@ func (h *ResidenceHandler) GenerateShareCode(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
@@ -206,11 +207,11 @@ func (h *ResidenceHandler) JoinWithCode(c *gin.Context) {
if err != nil {
switch {
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):
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):
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
c.JSON(http.StatusConflict, gin.H{"error": i18n.LocalizedMessage(c, "error.user_already_member")})
default:
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)
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
}
@@ -234,9 +235,9 @@ func (h *ResidenceHandler) GetResidenceUsers(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
default:
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)
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
}
userIDToRemove, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
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
}
@@ -266,18 +267,18 @@ func (h *ResidenceHandler) RemoveResidenceUser(c *gin.Context) {
if err != nil {
switch {
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):
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):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.cannot_remove_owner")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -298,7 +299,7 @@ func (h *ResidenceHandler) GenerateTasksReport(c *gin.Context) {
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -313,9 +314,9 @@ func (h *ResidenceHandler) GenerateTasksReport(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
@@ -360,11 +361,11 @@ func (h *ResidenceHandler) GenerateTasksReport(c *gin.Context) {
}
// Build response message
message := "Tasks report generated successfully"
message := i18n.LocalizedMessage(c, "message.tasks_report_generated")
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 {
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{

View File

@@ -5,6 +5,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/treytartt/casera-api/internal/i18n"
"github.com/treytartt/casera-api/internal/services"
)
@@ -34,37 +35,37 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
// Get all lookup data
residenceTypes, err := h.residenceService.GetResidenceTypes()
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
}
taskCategories, err := h.taskService.GetCategories()
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
}
taskPriorities, err := h.taskService.GetPriorities()
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
}
taskFrequencies, err := h.taskService.GetFrequencies()
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
}
taskStatuses, err := h.taskService.GetStatuses()
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
}
contractorSpecialties, err := h.contractorService.GetSpecialties()
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
}
@@ -83,7 +84,7 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
// Kept for API compatibility with mobile clients
func (h *StaticDataHandler) RefreshStaticData(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Static data refreshed",
"message": i18n.LocalizedMessage(c, "message.static_data_refreshed"),
"status": "success",
})
}

View File

@@ -6,6 +6,7 @@ import (
"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/models"
"github.com/treytartt/casera-api/internal/services"
@@ -54,7 +55,7 @@ func (h *SubscriptionHandler) GetUpgradeTrigger(c *gin.Context) {
trigger, err := h.subscriptionService.GetUpgradeTrigger(key)
if err != nil {
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
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@@ -115,13 +116,13 @@ func (h *SubscriptionHandler) ProcessPurchase(c *gin.Context) {
switch req.Platform {
case "ios":
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
}
subscription, err = h.subscriptionService.ProcessApplePurchase(user.ID, req.ReceiptData)
case "android":
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
}
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{
"message": "Subscription upgraded successfully",
"message": i18n.LocalizedMessage(c, "message.subscription_upgraded"),
"subscription": subscription,
})
}
@@ -149,7 +150,7 @@ func (h *SubscriptionHandler) CancelSubscription(c *gin.Context) {
}
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,
})
}
@@ -181,7 +182,7 @@ func (h *SubscriptionHandler) RestoreSubscription(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"message": "Subscription restored successfully",
"message": i18n.LocalizedMessage(c, "message.subscription_restored"),
"subscription": subscription,
})
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/shopspring/decimal"
"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/models"
"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)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -55,9 +56,9 @@ func (h *TaskHandler) GetTask(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
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)
residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32)
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
}
@@ -86,7 +87,7 @@ func (h *TaskHandler) GetTasksByResidence(c *gin.Context) {
if err != nil {
switch {
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:
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)
if err != nil {
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
}
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)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -135,9 +136,9 @@ func (h *TaskHandler) UpdateTask(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
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)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -159,15 +160,15 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -175,7 +176,7 @@ func (h *TaskHandler) MarkInProgress(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -183,15 +184,15 @@ func (h *TaskHandler) MarkInProgress(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -199,7 +200,7 @@ func (h *TaskHandler) CancelTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -207,17 +208,17 @@ func (h *TaskHandler) CancelTask(c *gin.Context) {
if err != nil {
switch {
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):
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):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.task_already_cancelled")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -225,7 +226,7 @@ func (h *TaskHandler) UncancelTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -233,15 +234,15 @@ func (h *TaskHandler) UncancelTask(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -249,7 +250,7 @@ func (h *TaskHandler) ArchiveTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -257,17 +258,17 @@ func (h *TaskHandler) ArchiveTask(c *gin.Context) {
if err != nil {
switch {
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):
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):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.task_already_archived")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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/
@@ -275,7 +276,7 @@ func (h *TaskHandler) UnarchiveTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -283,15 +284,15 @@ func (h *TaskHandler) UnarchiveTask(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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 ===
@@ -301,7 +302,7 @@ func (h *TaskHandler) GetTaskCompletions(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -309,9 +310,9 @@ func (h *TaskHandler) GetTaskCompletions(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
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)
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -344,9 +345,9 @@ func (h *TaskHandler) GetCompletion(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
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") {
// Parse multipart form
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
}
// Parse task_id (required)
taskIDStr := c.PostForm("task_id")
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
}
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
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
}
req.TaskID = uint(taskID)
@@ -416,7 +417,7 @@ func (h *TaskHandler) CreateCompletion(c *gin.Context) {
if h.storageService != nil {
result, err := h.storageService.Upload(file, "completions")
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
}
req.ImageURLs = append(req.ImageURLs, result.URL)
@@ -434,9 +435,9 @@ func (h *TaskHandler) CreateCompletion(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
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)
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
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
}
@@ -458,15 +459,15 @@ func (h *TaskHandler) DeleteCompletion(c *gin.Context) {
if err != nil {
switch {
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):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
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 ===

View File

@@ -5,6 +5,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/treytartt/casera-api/internal/i18n"
"github.com/treytartt/casera-api/internal/services"
)
@@ -23,7 +24,7 @@ func NewUploadHandler(storageService *services.StorageService) *UploadHandler {
func (h *UploadHandler) UploadImage(c *gin.Context) {
file, err := c.FormFile("file")
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
}
@@ -44,7 +45,7 @@ func (h *UploadHandler) UploadImage(c *gin.Context) {
func (h *UploadHandler) UploadDocument(c *gin.Context) {
file, err := c.FormFile("file")
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
}
@@ -62,7 +63,7 @@ func (h *UploadHandler) UploadDocument(c *gin.Context) {
func (h *UploadHandler) UploadCompletion(c *gin.Context) {
file, err := c.FormFile("file")
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
}
@@ -92,5 +93,5 @@ func (h *UploadHandler) DeleteFile(c *gin.Context) {
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/treytartt/casera-api/internal/i18n"
"github.com/treytartt/casera-api/internal/middleware"
"github.com/treytartt/casera-api/internal/models"
"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)
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
}
@@ -54,7 +55,7 @@ func (h *UserHandler) GetUser(c *gin.Context) {
targetUser, err := h.userService.GetUserIfSharedResidence(uint(userID), user.ID)
if err != nil {
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
}
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/config"
"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/push"
"github.com/treytartt/casera-api/internal/repositories"
@@ -48,6 +49,7 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
r.Use(utils.GinRecovery())
r.Use(utils.GinLogger())
r.Use(corsMiddleware(cfg))
r.Use(i18n.Middleware())
// Health check endpoint (no auth required)
r.GET("/api/health/", healthCheck)