i18n: backend-localized lookups, suggestions, and static data (10 languages)
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Backend CI / Build (push) Has been cancelled

- suggestion_service: fix scorer (stringList unmarshal accepts scalar|array;
  anchor scoring on base universal score so bool matches no longer tie); add
  localizeReasons for human-readable, Accept-Language-localized match reasons
- lookup_i18n: localize lookup display names, home-profile options, document
  types/categories via internal/i18n
- static_data_handler: per-locale seeded-data response (display_name, home
  profile options, document types/categories) with per-locale cache + ETag
- settings_handler: invalidate per-locale seeded-data cache on lookup change
  instead of pre-warming a single non-localized blob
- cache_service: per-locale seeded-data keys + ETag
- DTOs: add DisplayName fields (task/residence/contractor)
- translations: add suggestion.reason.* and lookup.* keys across all 10 langs
- cmd/api: extract startup helpers + tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-06-04 20:54:54 -05:00
parent 25897e913e
commit 12de5a230a
23 changed files with 1671 additions and 703 deletions
+32
View File
@@ -0,0 +1,32 @@
package main
import "time"
// shouldInitEmail returns true if email config has host and user set.
func shouldInitEmail(host, user string) bool {
return host != "" && user != ""
}
// shouldInitStorage returns true if upload directory is configured.
func shouldInitStorage(uploadDir string) bool {
return uploadDir != ""
}
// shouldInitEncryption returns true if encryption key is set.
func shouldInitEncryption(encryptionKey string) bool {
return encryptionKey != ""
}
// connectWithRetry attempts a connection with exponential backoff.
// Returns nil on success or the last error after all retries fail.
func connectWithRetry(connect func() error, maxRetries int) error {
var err error
for i := 0; i < maxRetries; i++ {
err = connect()
if err == nil {
return nil
}
time.Sleep(time.Duration(i+1) * time.Millisecond) // use ms in tests
}
return err
}
+107
View File
@@ -0,0 +1,107 @@
package main
import (
"errors"
"testing"
)
// --- shouldInitEmail ---
func TestShouldInitEmail_BothSet_True(t *testing.T) {
if !shouldInitEmail("smtp.example.com", "user@example.com") {
t.Error("expected true when both set")
}
}
func TestShouldInitEmail_MissingHost_False(t *testing.T) {
if shouldInitEmail("", "user@example.com") {
t.Error("expected false when host empty")
}
}
func TestShouldInitEmail_MissingUser_False(t *testing.T) {
if shouldInitEmail("smtp.example.com", "") {
t.Error("expected false when user empty")
}
}
func TestShouldInitEmail_BothEmpty_False(t *testing.T) {
if shouldInitEmail("", "") {
t.Error("expected false when both empty")
}
}
// --- shouldInitStorage ---
func TestShouldInitStorage_Set_True(t *testing.T) {
if !shouldInitStorage("/uploads") {
t.Error("expected true")
}
}
func TestShouldInitStorage_Empty_False(t *testing.T) {
if shouldInitStorage("") {
t.Error("expected false")
}
}
// --- shouldInitEncryption ---
func TestShouldInitEncryption_Set_True(t *testing.T) {
if !shouldInitEncryption("secret-key-123") {
t.Error("expected true")
}
}
func TestShouldInitEncryption_Empty_False(t *testing.T) {
if shouldInitEncryption("") {
t.Error("expected false")
}
}
// --- connectWithRetry ---
func TestConnectWithRetry_SucceedsFirst_NoRetry(t *testing.T) {
calls := 0
err := connectWithRetry(func() error {
calls++
return nil
}, 3)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if calls != 1 {
t.Errorf("calls = %d, want 1", calls)
}
}
func TestConnectWithRetry_SucceedsSecond_OneRetry(t *testing.T) {
calls := 0
err := connectWithRetry(func() error {
calls++
if calls == 1 {
return errors.New("fail")
}
return nil
}, 3)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if calls != 2 {
t.Errorf("calls = %d, want 2", calls)
}
}
func TestConnectWithRetry_AllFail_ReturnsError(t *testing.T) {
calls := 0
err := connectWithRetry(func() error {
calls++
return errors.New("fail")
}, 3)
if err == nil {
t.Error("expected error")
}
if calls != 3 {
t.Errorf("calls = %d, want 3", calls)
}
}
+8 -125
View File
@@ -248,137 +248,20 @@ func (h *AdminSettingsHandler) cacheAllLookups(ctx context.Context) (bool, error
} }
log.Debug().Int("count", len(taskTemplates)).Msg("Cached task templates") log.Debug().Int("count", len(taskTemplates)).Msg("Cached task templates")
// Build and cache the unified seeded data response // Invalidate the unified seeded-data cache for every locale. The combined
// Import the grouped response type // response is localized (lookup display_name + home-profile options) and is
seededData := map[string]interface{}{ // rebuilt per-locale on demand by the static_data handler, so the correct
"residence_types": residenceTypes, // action after a lookup change is to clear all language variants rather than
"task_categories": categories, // pre-warm a single (non-localized) blob.
"task_priorities": priorities, if err := cache.InvalidateSeededData(ctx); err != nil {
"task_frequencies": frequencies, return false, fmt.Errorf("failed to invalidate seeded data: %w", err)
"contractor_specialties": specialties,
"task_templates": buildGroupedTemplates(taskTemplates),
} }
log.Debug().Msg("Invalidated per-locale seeded data cache")
etag, err := cache.CacheSeededData(ctx, seededData)
if err != nil {
return false, fmt.Errorf("failed to cache seeded data: %w", err)
}
log.Debug().Str("etag", etag).Msg("Cached unified seeded data")
log.Info().Msg("All lookup data cached in Redis successfully") log.Info().Msg("All lookup data cached in Redis successfully")
return true, nil return true, nil
} }
// buildGroupedTemplates groups task templates by category for the seeded data response
func buildGroupedTemplates(templates []models.TaskTemplate) map[string]interface{} {
type templateResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
CategoryID *uint `json:"category_id"`
Category map[string]interface{} `json:"category,omitempty"`
FrequencyID *uint `json:"frequency_id"`
Frequency map[string]interface{} `json:"frequency,omitempty"`
IconIOS string `json:"icon_ios"`
IconAndroid string `json:"icon_android"`
Tags []string `json:"tags"`
DisplayOrder int `json:"display_order"`
IsActive bool `json:"is_active"`
}
type categoryGroup struct {
CategoryName string `json:"category_name"`
CategoryID *uint `json:"category_id"`
Templates []templateResponse `json:"templates"`
Count int `json:"count"`
}
categoryMap := make(map[string]*categoryGroup)
categoryOrder := []string{}
for _, t := range templates {
categoryName := "Uncategorized"
var categoryID *uint
if t.Category != nil {
categoryName = t.Category.Name
categoryID = &t.Category.ID
}
if _, exists := categoryMap[categoryName]; !exists {
categoryMap[categoryName] = &categoryGroup{
CategoryName: categoryName,
CategoryID: categoryID,
Templates: []templateResponse{},
}
categoryOrder = append(categoryOrder, categoryName)
}
resp := templateResponse{
ID: t.ID,
Title: t.Title,
Description: t.Description,
CategoryID: t.CategoryID,
FrequencyID: t.FrequencyID,
IconIOS: t.IconIOS,
IconAndroid: t.IconAndroid,
Tags: parseTags(t.Tags),
DisplayOrder: t.DisplayOrder,
IsActive: t.IsActive,
}
if t.Category != nil {
resp.Category = map[string]interface{}{
"id": t.Category.ID,
"name": t.Category.Name,
"description": t.Category.Description,
"icon": t.Category.Icon,
"color": t.Category.Color,
"display_order": t.Category.DisplayOrder,
}
}
if t.Frequency != nil {
resp.Frequency = map[string]interface{}{
"id": t.Frequency.ID,
"name": t.Frequency.Name,
"days": t.Frequency.Days,
"display_order": t.Frequency.DisplayOrder,
}
}
categoryMap[categoryName].Templates = append(categoryMap[categoryName].Templates, resp)
}
categories := make([]categoryGroup, len(categoryOrder))
totalCount := 0
for i, name := range categoryOrder {
group := categoryMap[name]
group.Count = len(group.Templates)
totalCount += group.Count
categories[i] = *group
}
return map[string]interface{}{
"categories": categories,
"total_count": totalCount,
}
}
// parseTags splits a comma-separated tags string into a slice
func parseTags(tags string) []string {
if tags == "" {
return []string{}
}
parts := strings.Split(tags, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
trimmed := strings.TrimSpace(p)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
// SeedTestData handles POST /api/admin/settings/seed-test-data // SeedTestData handles POST /api/admin/settings/seed-test-data
func (h *AdminSettingsHandler) SeedTestData(c echo.Context) error { func (h *AdminSettingsHandler) SeedTestData(c echo.Context) error {
if err := h.runSeedFile("002_test_data.sql"); err != nil { if err := h.runSeedFile("002_test_data.sql"); err != nil {
+3
View File
@@ -9,7 +9,10 @@ import (
// ContractorSpecialtyResponse represents a contractor specialty // ContractorSpecialtyResponse represents a contractor specialty
type ContractorSpecialtyResponse struct { type ContractorSpecialtyResponse struct {
ID uint `json:"id"` ID uint `json:"id"`
// Name is the stable English identifier (clients match on this).
Name string `json:"name"` Name string `json:"name"`
// DisplayName is the localized label for the request's Accept-Language.
DisplayName string `json:"display_name"`
Description string `json:"description"` Description string `json:"description"`
Icon string `json:"icon"` Icon string `json:"icon"`
DisplayOrder int `json:"display_order"` DisplayOrder int `json:"display_order"`
+3
View File
@@ -11,7 +11,10 @@ import (
// ResidenceTypeResponse represents a residence type in the API response // ResidenceTypeResponse represents a residence type in the API response
type ResidenceTypeResponse struct { type ResidenceTypeResponse struct {
ID uint `json:"id"` ID uint `json:"id"`
// Name is the stable English identifier (clients match on this).
Name string `json:"name"` Name string `json:"name"`
// DisplayName is the localized label for the request's Accept-Language.
DisplayName string `json:"display_name"`
} }
// ResidenceUserResponse represents a user with access to a residence // ResidenceUserResponse represents a user with access to a residence
+5
View File
@@ -14,7 +14,10 @@ import (
// TaskCategoryResponse represents a task category // TaskCategoryResponse represents a task category
type TaskCategoryResponse struct { type TaskCategoryResponse struct {
ID uint `json:"id"` ID uint `json:"id"`
// Name is the stable English identifier (clients match on this).
Name string `json:"name"` Name string `json:"name"`
// DisplayName is the localized label for the request's Accept-Language.
DisplayName string `json:"display_name"`
Description string `json:"description"` Description string `json:"description"`
Icon string `json:"icon"` Icon string `json:"icon"`
Color string `json:"color"` Color string `json:"color"`
@@ -25,6 +28,7 @@ type TaskCategoryResponse struct {
type TaskPriorityResponse struct { type TaskPriorityResponse struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
DisplayName string `json:"display_name"`
Level int `json:"level"` Level int `json:"level"`
Color string `json:"color"` Color string `json:"color"`
DisplayOrder int `json:"display_order"` DisplayOrder int `json:"display_order"`
@@ -34,6 +38,7 @@ type TaskPriorityResponse struct {
type TaskFrequencyResponse struct { type TaskFrequencyResponse struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
DisplayName string `json:"display_name"`
Days *int `json:"days"` Days *int `json:"days"`
DisplayOrder int `json:"display_order"` DisplayOrder int `json:"display_order"`
} }
+19 -5
View File
@@ -21,6 +21,9 @@ type SeededDataResponse struct {
TaskFrequencies interface{} `json:"task_frequencies"` TaskFrequencies interface{} `json:"task_frequencies"`
ContractorSpecialties interface{} `json:"contractor_specialties"` ContractorSpecialties interface{} `json:"contractor_specialties"`
TaskTemplates responses.TaskTemplatesGroupedResponse `json:"task_templates"` TaskTemplates responses.TaskTemplatesGroupedResponse `json:"task_templates"`
HomeProfileOptions map[string][]services.HomeProfileOption `json:"home_profile_options"`
DocumentTypes []services.HomeProfileOption `json:"document_types"`
DocumentCategories []services.HomeProfileOption `json:"document_categories"`
} }
// StaticDataHandler handles static/lookup data endpoints // StaticDataHandler handles static/lookup data endpoints
@@ -54,13 +57,18 @@ func NewStaticDataHandler(
func (h *StaticDataHandler) GetStaticData(c echo.Context) error { func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
// Lookup display labels and home-profile options are localized for the
// request's language, so the cache + ETag are keyed by locale.
locale := i18n.GetLocale(c)
localizer := i18n.GetLocalizer(c)
// Check If-None-Match header for conditional request // Check If-None-Match header for conditional request
// Strip W/ prefix if present (added by reverse proxy, but we store without it) // Strip W/ prefix if present (added by reverse proxy, but we store without it)
clientETag := strings.TrimPrefix(c.Request().Header.Get("If-None-Match"), "W/") clientETag := strings.TrimPrefix(c.Request().Header.Get("If-None-Match"), "W/")
// Try to get cached ETag first (fast path for 304 responses) // Try to get cached ETag first (fast path for 304 responses)
if h.cache != nil && clientETag != "" { if h.cache != nil && clientETag != "" {
cachedETag, err := h.cache.GetSeededDataETag(ctx) cachedETag, err := h.cache.GetSeededDataETag(ctx, locale)
if err == nil && cachedETag == clientETag { if err == nil && cachedETag == clientETag {
// Client has the latest data, return 304 Not Modified // Client has the latest data, return 304 Not Modified
return c.NoContent(http.StatusNotModified) return c.NoContent(http.StatusNotModified)
@@ -70,10 +78,10 @@ func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
// Try to get cached seeded data // Try to get cached seeded data
if h.cache != nil { if h.cache != nil {
var cachedData SeededDataResponse var cachedData SeededDataResponse
err := h.cache.GetCachedSeededData(ctx, &cachedData) err := h.cache.GetCachedSeededData(ctx, locale, &cachedData)
if err == nil { if err == nil {
// Cache hit - get the ETag and return data // Cache hit - get the ETag and return data
etag, etagErr := h.cache.GetSeededDataETag(ctx) etag, etagErr := h.cache.GetSeededDataETag(ctx, locale)
if etagErr == nil { if etagErr == nil {
c.Response().Header().Set("ETag", etag) c.Response().Header().Set("ETag", etag)
c.Response().Header().Set("Cache-Control", "private, max-age=3600") c.Response().Header().Set("Cache-Control", "private, max-age=3600")
@@ -116,6 +124,9 @@ func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
return err return err
} }
// Localize the lookup display_name fields in place for this request's locale.
services.LocalizeLookups(localizer, residenceTypes, taskCategories, taskPriorities, taskFrequencies, contractorSpecialties)
// Build response // Build response
seededData := SeededDataResponse{ seededData := SeededDataResponse{
ResidenceTypes: residenceTypes, ResidenceTypes: residenceTypes,
@@ -124,11 +135,14 @@ func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
TaskFrequencies: taskFrequencies, TaskFrequencies: taskFrequencies,
ContractorSpecialties: contractorSpecialties, ContractorSpecialties: contractorSpecialties,
TaskTemplates: taskTemplates, TaskTemplates: taskTemplates,
HomeProfileOptions: services.BuildHomeProfileOptions(localizer),
DocumentTypes: services.BuildDocumentTypes(localizer),
DocumentCategories: services.BuildDocumentCategories(localizer),
} }
// Cache the data and get ETag // Cache the data and get ETag (per-locale)
if h.cache != nil { if h.cache != nil {
etag, cacheErr := h.cache.CacheSeededData(ctx, seededData) etag, cacheErr := h.cache.CacheSeededData(ctx, locale, seededData)
if cacheErr != nil { if cacheErr != nil {
log.Warn().Err(cacheErr).Msg("Failed to cache seeded data") log.Warn().Err(cacheErr).Msg("Failed to cache seeded data")
} else { } else {
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/treytartt/honeydue-api/internal/apperrors" "github.com/treytartt/honeydue-api/internal/apperrors"
"github.com/treytartt/honeydue-api/internal/i18n"
"github.com/treytartt/honeydue-api/internal/middleware" "github.com/treytartt/honeydue-api/internal/middleware"
"github.com/treytartt/honeydue-api/internal/services" "github.com/treytartt/honeydue-api/internal/services"
) )
@@ -41,7 +42,7 @@ func (h *SuggestionHandler) GetSuggestions(c echo.Context) error {
return apperrors.BadRequest("error.invalid_id") return apperrors.BadRequest("error.invalid_id")
} }
resp, err := h.suggestionService.GetSuggestions(uint(residenceID), user.ID) resp, err := h.suggestionService.GetSuggestions(uint(residenceID), user.ID, i18n.GetLocalizer(c))
if err != nil { if err != nil {
return err return err
} }
+93 -37
View File
@@ -25,7 +25,6 @@
"error.google_signin_not_configured": "Google-Anmeldung ist nicht konfiguriert", "error.google_signin_not_configured": "Google-Anmeldung ist nicht konfiguriert",
"error.google_signin_failed": "Google-Anmeldung fehlgeschlagen", "error.google_signin_failed": "Google-Anmeldung fehlgeschlagen",
"error.invalid_google_token": "Ungultiger Google-Identitats-Token", "error.invalid_google_token": "Ungultiger Google-Identitats-Token",
"error.invalid_task_id": "Ungultige Aufgaben-ID", "error.invalid_task_id": "Ungultige Aufgaben-ID",
"error.invalid_residence_id": "Ungultige Immobilien-ID", "error.invalid_residence_id": "Ungultige Immobilien-ID",
"error.invalid_contractor_id": "Ungultige Dienstleister-ID", "error.invalid_contractor_id": "Ungultige Dienstleister-ID",
@@ -34,7 +33,6 @@
"error.invalid_user_id": "Ungultige Benutzer-ID", "error.invalid_user_id": "Ungultige Benutzer-ID",
"error.invalid_notification_id": "Ungultige Benachrichtigungs-ID", "error.invalid_notification_id": "Ungultige Benachrichtigungs-ID",
"error.invalid_device_id": "Ungultige Gerate-ID", "error.invalid_device_id": "Ungultige Gerate-ID",
"error.task_not_found": "Aufgabe nicht gefunden", "error.task_not_found": "Aufgabe nicht gefunden",
"error.residence_not_found": "Immobilie nicht gefunden", "error.residence_not_found": "Immobilie nicht gefunden",
"error.contractor_not_found": "Dienstleister nicht gefunden", "error.contractor_not_found": "Dienstleister nicht gefunden",
@@ -43,7 +41,6 @@
"error.user_not_found": "Benutzer nicht gefunden", "error.user_not_found": "Benutzer nicht gefunden",
"error.share_code_invalid": "Ungultiger Freigabecode", "error.share_code_invalid": "Ungultiger Freigabecode",
"error.share_code_expired": "Der Freigabecode ist abgelaufen", "error.share_code_expired": "Der Freigabecode ist abgelaufen",
"error.task_access_denied": "Sie haben keinen Zugriff auf diese Aufgabe", "error.task_access_denied": "Sie haben keinen Zugriff auf diese Aufgabe",
"error.residence_access_denied": "Sie haben keinen Zugriff auf diese Immobilie", "error.residence_access_denied": "Sie haben keinen Zugriff auf diese Immobilie",
"error.contractor_access_denied": "Sie haben keinen Zugriff auf diesen Dienstleister", "error.contractor_access_denied": "Sie haben keinen Zugriff auf diesen Dienstleister",
@@ -52,10 +49,8 @@
"error.cannot_remove_owner": "Der Eigentumer kann nicht entfernt werden", "error.cannot_remove_owner": "Der Eigentumer kann nicht entfernt werden",
"error.user_already_member": "Der Benutzer ist bereits Mitglied dieser Immobilie", "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.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_cancelled": "Die Aufgabe ist bereits storniert",
"error.task_already_archived": "Die Aufgabe ist bereits archiviert", "error.task_already_archived": "Die Aufgabe ist bereits archiviert",
"error.failed_to_parse_form": "Formular konnte nicht analysiert werden", "error.failed_to_parse_form": "Formular konnte nicht analysiert werden",
"error.task_id_required": "task_id ist erforderlich", "error.task_id_required": "task_id ist erforderlich",
"error.invalid_task_id_value": "Ungultige task_id", "error.invalid_task_id_value": "Ungultige task_id",
@@ -64,14 +59,12 @@
"error.invalid_residence_id_value": "Ungultige residence_id", "error.invalid_residence_id_value": "Ungultige residence_id",
"error.title_required": "Titel ist erforderlich", "error.title_required": "Titel ist erforderlich",
"error.failed_to_upload_file": "Datei konnte nicht hochgeladen werden", "error.failed_to_upload_file": "Datei konnte nicht hochgeladen werden",
"message.logged_out": "Erfolgreich abgemeldet", "message.logged_out": "Erfolgreich abgemeldet",
"message.email_verified": "E-Mail erfolgreich verifiziert", "message.email_verified": "E-Mail erfolgreich verifiziert",
"message.verification_email_sent": "Verifizierungs-E-Mail gesendet", "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.password_reset_email_sent": "Wenn ein Konto mit dieser E-Mail existiert, wurde ein Zurucksetzungscode gesendet.",
"message.reset_code_verified": "Code erfolgreich verifiziert", "message.reset_code_verified": "Code erfolgreich verifiziert",
"message.password_reset_success": "Passwort erfolgreich zuruckgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an.", "message.password_reset_success": "Passwort erfolgreich zuruckgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an.",
"message.task_deleted": "Aufgabe erfolgreich geloscht", "message.task_deleted": "Aufgabe erfolgreich geloscht",
"message.task_in_progress": "Aufgabe als in Bearbeitung markiert", "message.task_in_progress": "Aufgabe als in Bearbeitung markiert",
"message.task_cancelled": "Aufgabe storniert", "message.task_cancelled": "Aufgabe storniert",
@@ -79,46 +72,35 @@
"message.task_archived": "Aufgabe archiviert", "message.task_archived": "Aufgabe archiviert",
"message.task_unarchived": "Aufgabe dearchiviert", "message.task_unarchived": "Aufgabe dearchiviert",
"message.completion_deleted": "Abschluss erfolgreich geloscht", "message.completion_deleted": "Abschluss erfolgreich geloscht",
"message.residence_deleted": "Immobilie erfolgreich geloscht", "message.residence_deleted": "Immobilie erfolgreich geloscht",
"message.user_removed": "Benutzer von der Immobilie entfernt", "message.user_removed": "Benutzer von der Immobilie entfernt",
"message.tasks_report_generated": "Aufgabenbericht erfolgreich erstellt", "message.tasks_report_generated": "Aufgabenbericht erfolgreich erstellt",
"message.tasks_report_sent": "Aufgabenbericht erstellt und an {{.Email}} gesendet", "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.tasks_report_email_failed": "Aufgabenbericht erstellt, aber E-Mail konnte nicht gesendet werden",
"message.contractor_deleted": "Dienstleister erfolgreich geloscht", "message.contractor_deleted": "Dienstleister erfolgreich geloscht",
"message.document_deleted": "Dokument erfolgreich geloscht", "message.document_deleted": "Dokument erfolgreich geloscht",
"message.document_activated": "Dokument aktiviert", "message.document_activated": "Dokument aktiviert",
"message.document_deactivated": "Dokument deaktiviert", "message.document_deactivated": "Dokument deaktiviert",
"message.notification_marked_read": "Benachrichtigung als gelesen markiert", "message.notification_marked_read": "Benachrichtigung als gelesen markiert",
"message.all_notifications_marked_read": "Alle Benachrichtigungen als gelesen markiert", "message.all_notifications_marked_read": "Alle Benachrichtigungen als gelesen markiert",
"message.device_removed": "Gerät entfernt", "message.device_removed": "Gerät entfernt",
"message.subscription_upgraded": "Abonnement erfolgreich aktualisiert", "message.subscription_upgraded": "Abonnement erfolgreich aktualisiert",
"message.subscription_cancelled": "Abonnement gekündigt. Sie behalten die Pro-Vorteile bis zum Ende Ihres Abrechnungszeitraums.", "message.subscription_cancelled": "Abonnement gekündigt. Sie behalten die Pro-Vorteile bis zum Ende Ihres Abrechnungszeitraums.",
"message.subscription_restored": "Abonnement erfolgreich wiederhergestellt", "message.subscription_restored": "Abonnement erfolgreich wiederhergestellt",
"message.file_deleted": "Datei erfolgreich gelöscht", "message.file_deleted": "Datei erfolgreich gelöscht",
"message.static_data_refreshed": "Statische Daten aktualisiert", "message.static_data_refreshed": "Statische Daten aktualisiert",
"error.notification_not_found": "Benachrichtigung nicht gefunden", "error.notification_not_found": "Benachrichtigung nicht gefunden",
"error.invalid_platform": "Ungültige Plattform", "error.invalid_platform": "Ungültige Plattform",
"error.upgrade_trigger_not_found": "Upgrade-Trigger nicht gefunden", "error.upgrade_trigger_not_found": "Upgrade-Trigger nicht gefunden",
"error.receipt_data_required": "receipt_data ist für iOS erforderlich", "error.receipt_data_required": "receipt_data ist für iOS erforderlich",
"error.purchase_token_required": "purchase_token ist für Android erforderlich", "error.purchase_token_required": "purchase_token ist für Android erforderlich",
"error.no_file_provided": "Keine Datei bereitgestellt", "error.no_file_provided": "Keine Datei bereitgestellt",
"error.failed_to_fetch_residence_types": "Fehler beim Abrufen der Immobilientypen", "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_categories": "Fehler beim Abrufen der Aufgabenkategorien",
"error.failed_to_fetch_task_priorities": "Fehler beim Abrufen der Aufgabenprioritäten", "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_frequencies": "Fehler beim Abrufen der Aufgabenfrequenzen",
"error.failed_to_fetch_task_statuses": "Fehler beim Abrufen der Aufgabenstatus", "error.failed_to_fetch_task_statuses": "Fehler beim Abrufen der Aufgabenstatus",
"error.failed_to_fetch_contractor_specialties": "Fehler beim Abrufen der Dienstleister-Spezialitäten", "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.title": "Aufgabe Bald Fallig",
"push.task_due_soon.body": "{{.TaskTitle}} ist fallig am {{.DueDate}}", "push.task_due_soon.body": "{{.TaskTitle}} ist fallig am {{.DueDate}}",
"push.task_overdue.title": "Uberfällige Aufgabe", "push.task_overdue.title": "Uberfällige Aufgabe",
@@ -129,63 +111,137 @@
"push.task_assigned.body": "Ihnen wurde {{.TaskTitle}} zugewiesen", "push.task_assigned.body": "Ihnen wurde {{.TaskTitle}} zugewiesen",
"push.residence_shared.title": "Immobilie Geteilt", "push.residence_shared.title": "Immobilie Geteilt",
"push.residence_shared.body": "{{.UserName}} hat {{.ResidenceName}} mit Ihnen geteilt", "push.residence_shared.body": "{{.UserName}} hat {{.ResidenceName}} mit Ihnen geteilt",
"email.welcome.subject": "Willkommen bei honeyDue!", "email.welcome.subject": "Willkommen bei honeyDue!",
"email.verification.subject": "Bestatigen Sie Ihre E-Mail", "email.verification.subject": "Bestatigen Sie Ihre E-Mail",
"email.password_reset.subject": "Passwort-Zurucksetzungscode", "email.password_reset.subject": "Passwort-Zurucksetzungscode",
"email.tasks_report.subject": "Aufgabenbericht fur {{.ResidenceName}}", "email.tasks_report.subject": "Aufgabenbericht fur {{.ResidenceName}}",
"lookup.residence_type.house": "Haus", "lookup.residence_type.house": "Haus",
"lookup.residence_type.apartment": "Wohnung", "lookup.residence_type.apartment": "Wohnung",
"lookup.residence_type.condo": "Eigentumswohnung", "lookup.residence_type.condo": "Eigentumswohnung",
"lookup.residence_type.townhouse": "Reihenhaus", "lookup.residence_type.townhouse": "Reihenhaus",
"lookup.residence_type.mobile_home": "Mobilheim", "lookup.residence_type.mobile_home": "Mobilheim",
"lookup.residence_type.other": "Sonstiges", "lookup.residence_type.other": "Sonstiges",
"lookup.task_category.plumbing": "Sanitär", "lookup.task_category.plumbing": "Sanitär",
"lookup.task_category.electrical": "Elektrik", "lookup.task_category.electrical": "Elektrik",
"lookup.task_category.hvac": "Heizung/Klimaanlage", "lookup.task_category.hvac": "HLK",
"lookup.task_category.appliances": "Gerate", "lookup.task_category.appliances": "Haushaltsgeräte",
"lookup.task_category.exterior": "Aussenbereich", "lookup.task_category.exterior": "Außenbereich",
"lookup.task_category.interior": "Innenbereich", "lookup.task_category.interior": "Innenbereich",
"lookup.task_category.landscaping": "Gartenpflege", "lookup.task_category.landscaping": "Gartenpflege",
"lookup.task_category.safety": "Sicherheit", "lookup.task_category.safety": "Sicherheit",
"lookup.task_category.cleaning": "Reinigung", "lookup.task_category.cleaning": "Reinigung",
"lookup.task_category.pest_control": "Schadlingsbekampfung", "lookup.task_category.pest_control": "Schädlingsbekämpfung",
"lookup.task_category.seasonal": "Saisonal", "lookup.task_category.seasonal": "Saisonal",
"lookup.task_category.other": "Sonstiges", "lookup.task_category.other": "Sonstiges",
"lookup.task_priority.low": "Niedrig", "lookup.task_priority.low": "Niedrig",
"lookup.task_priority.medium": "Mittel", "lookup.task_priority.medium": "Mittel",
"lookup.task_priority.high": "Hoch", "lookup.task_priority.high": "Hoch",
"lookup.task_priority.urgent": "Dringend", "lookup.task_priority.urgent": "Dringend",
"lookup.task_status.pending": "Ausstehend", "lookup.task_status.pending": "Ausstehend",
"lookup.task_status.in_progress": "In Bearbeitung", "lookup.task_status.in_progress": "In Bearbeitung",
"lookup.task_status.completed": "Abgeschlossen", "lookup.task_status.completed": "Abgeschlossen",
"lookup.task_status.cancelled": "Storniert", "lookup.task_status.cancelled": "Storniert",
"lookup.task_status.archived": "Archiviert", "lookup.task_status.archived": "Archiviert",
"lookup.task_frequency.once": "Einmalig", "lookup.task_frequency.once": "Einmalig",
"lookup.task_frequency.daily": "Taglich", "lookup.task_frequency.daily": "Täglich",
"lookup.task_frequency.weekly": "Wochentlich", "lookup.task_frequency.weekly": "Wöchentlich",
"lookup.task_frequency.biweekly": "Alle 2 Wochen", "lookup.task_frequency.biweekly": "Alle 2 Wochen",
"lookup.task_frequency.monthly": "Monatlich", "lookup.task_frequency.monthly": "Monatlich",
"lookup.task_frequency.quarterly": "Vierteljahrlich", "lookup.task_frequency.quarterly": "Vierteljährlich",
"lookup.task_frequency.semiannually": "Halbjahrlich", "lookup.task_frequency.semiannually": "Halbjahrlich",
"lookup.task_frequency.annually": "Jahrlich", "lookup.task_frequency.annually": "Jährlich",
"lookup.contractor_specialty.plumber": "Klempner", "lookup.contractor_specialty.plumber": "Klempner",
"lookup.contractor_specialty.electrician": "Elektriker", "lookup.contractor_specialty.electrician": "Elektriker",
"lookup.contractor_specialty.hvac_technician": "HLK-Techniker", "lookup.contractor_specialty.hvac_technician": "HLK-Techniker",
"lookup.contractor_specialty.handyman": "Handwerker", "lookup.contractor_specialty.handyman": "Handwerker",
"lookup.contractor_specialty.landscaper": "Landschaftsgartner", "lookup.contractor_specialty.landscaper": "Landschaftsgärtner",
"lookup.contractor_specialty.roofer": "Dachdecker", "lookup.contractor_specialty.roofer": "Dachdecker",
"lookup.contractor_specialty.painter": "Maler", "lookup.contractor_specialty.painter": "Maler",
"lookup.contractor_specialty.carpenter": "Schreiner", "lookup.contractor_specialty.carpenter": "Schreiner",
"lookup.contractor_specialty.pest_control": "Schadlingsbekampfung", "lookup.contractor_specialty.pest_control": "Schädlingsbekämpfung",
"lookup.contractor_specialty.cleaning": "Reinigung", "lookup.contractor_specialty.cleaning": "Reinigung",
"lookup.contractor_specialty.pool_service": "Pool-Service", "lookup.contractor_specialty.pool_service": "Poolservice",
"lookup.contractor_specialty.general_contractor": "Generalunternehmer", "lookup.contractor_specialty.general_contractor": "Generalunternehmer",
"lookup.contractor_specialty.other": "Sonstiges" "lookup.contractor_specialty.other": "Sonstiges",
"suggestion.reason.has_pool": "Ihr Zuhause hat einen Pool",
"suggestion.reason.has_sprinkler_system": "Ihr Zuhause hat eine Bewässerungsanlage",
"suggestion.reason.has_septic": "Ihr Zuhause hat eine Klärgrube",
"suggestion.reason.has_fireplace": "Ihr Zuhause hat einen Kamin",
"suggestion.reason.has_garage": "Ihr Zuhause hat eine Garage",
"suggestion.reason.has_basement": "Ihr Zuhause hat einen Keller",
"suggestion.reason.has_attic": "Ihr Zuhause hat einen Dachboden",
"suggestion.reason.heating_type": "Passt zu Ihrer Heizung",
"suggestion.reason.cooling_type": "Passt zu Ihrer Kühlung",
"suggestion.reason.water_heater_type": "Passt zu Ihrem Warmwasserbereiter",
"suggestion.reason.roof_type": "Passt zu Ihrem Dach",
"suggestion.reason.exterior_type": "Passt zu Ihrer Fassade",
"suggestion.reason.flooring_primary": "Passt zu Ihrem Bodenbelag",
"suggestion.reason.landscaping_type": "Passt zu Ihrer Gartengestaltung",
"suggestion.reason.property_type": "Empfohlen für Ihren Immobilientyp",
"suggestion.reason.climate_region": "Empfohlen für Ihr Klima",
"lookup.residence_type.duplex": "Doppelhaus",
"lookup.residence_type.vacation_home": "Ferienhaus",
"lookup.task_category.general": "Allgemein",
"lookup.task_frequency.bi_weekly": "Zweiwöchentlich",
"lookup.task_frequency.semi_annually": "Halbjährlich",
"lookup.task_frequency.custom": "Benutzerdefiniert",
"lookup.contractor_specialty.appliance_repair": "Gerätereparatur",
"lookup.contractor_specialty.cleaner": "Reinigungskraft",
"lookup.contractor_specialty.locksmith": "Schlosser",
"lookup.home_profile.gas_furnace": "Gasheizung",
"lookup.home_profile.electric_furnace": "Elektroheizung",
"lookup.home_profile.heat_pump": "Wärmepumpe",
"lookup.home_profile.boiler": "Heizkessel",
"lookup.home_profile.radiant": "Strahlungsheizung",
"lookup.home_profile.other": "Sonstiges",
"lookup.home_profile.central_ac": "Zentrale Klimaanlage",
"lookup.home_profile.window_ac": "Fensterklimagerät",
"lookup.home_profile.evaporative": "Verdunstung",
"lookup.home_profile.none": "Keine",
"lookup.home_profile.tank_gas": "Speicher (Gas)",
"lookup.home_profile.tank_electric": "Speicher (Elektro)",
"lookup.home_profile.tankless_gas": "Durchlauf (Gas)",
"lookup.home_profile.tankless_electric": "Durchlauf (Elektro)",
"lookup.home_profile.solar": "Solar",
"lookup.home_profile.asphalt_shingle": "Asphaltschindel",
"lookup.home_profile.metal": "Metall",
"lookup.home_profile.tile": "Ziegel",
"lookup.home_profile.slate": "Schiefer",
"lookup.home_profile.wood_shake": "Holzschindel",
"lookup.home_profile.flat": "Flach",
"lookup.home_profile.brick": "Backstein",
"lookup.home_profile.vinyl_siding": "Vinylverkleidung",
"lookup.home_profile.wood_siding": "Holzverkleidung",
"lookup.home_profile.stucco": "Putz",
"lookup.home_profile.stone": "Stein",
"lookup.home_profile.fiber_cement": "Faserzement",
"lookup.home_profile.hardwood": "Hartholz",
"lookup.home_profile.laminate": "Laminat",
"lookup.home_profile.carpet": "Teppich",
"lookup.home_profile.vinyl": "Vinyl",
"lookup.home_profile.concrete": "Beton",
"lookup.home_profile.lawn": "Rasen",
"lookup.home_profile.desert": "Wüste",
"lookup.home_profile.xeriscape": "Xeriscaping",
"lookup.home_profile.garden": "Garten",
"lookup.home_profile.mixed": "Gemischt",
"lookup.document_type.warranty": "Garantie",
"lookup.document_type.manual": "Benutzerhandbuch",
"lookup.document_type.receipt": "Beleg/Rechnung",
"lookup.document_type.inspection": "Inspektionsbericht",
"lookup.document_type.permit": "Genehmigung",
"lookup.document_type.deed": "Urkunde/Titel",
"lookup.document_type.insurance": "Versicherung",
"lookup.document_type.contract": "Vertrag",
"lookup.document_type.photo": "Foto",
"lookup.document_type.other": "Sonstiges",
"lookup.document_category.appliance": "Haushaltsgerät",
"lookup.document_category.hvac": "HLK",
"lookup.document_category.plumbing": "Sanitär",
"lookup.document_category.electrical": "Elektrik",
"lookup.document_category.roofing": "Dach",
"lookup.document_category.structural": "Struktur",
"lookup.document_category.landscaping": "Gartengestaltung",
"lookup.document_category.general": "Allgemein",
"lookup.document_category.other": "Sonstiges"
} }
+82 -26
View File
@@ -28,7 +28,6 @@
"error.google_signin_not_configured": "Google Sign In is not configured", "error.google_signin_not_configured": "Google Sign In is not configured",
"error.google_signin_failed": "Google Sign In failed", "error.google_signin_failed": "Google Sign In failed",
"error.invalid_google_token": "Invalid Google identity token", "error.invalid_google_token": "Invalid Google identity token",
"error.invalid_task_id": "Invalid task ID", "error.invalid_task_id": "Invalid task ID",
"error.invalid_residence_id": "Invalid residence ID", "error.invalid_residence_id": "Invalid residence ID",
"error.invalid_contractor_id": "Invalid contractor ID", "error.invalid_contractor_id": "Invalid contractor ID",
@@ -37,7 +36,6 @@
"error.invalid_user_id": "Invalid user ID", "error.invalid_user_id": "Invalid user ID",
"error.invalid_notification_id": "Invalid notification ID", "error.invalid_notification_id": "Invalid notification ID",
"error.invalid_device_id": "Invalid device ID", "error.invalid_device_id": "Invalid device ID",
"error.task_not_found": "Task not found", "error.task_not_found": "Task not found",
"error.residence_not_found": "Residence not found", "error.residence_not_found": "Residence not found",
"error.contractor_not_found": "Contractor not found", "error.contractor_not_found": "Contractor not found",
@@ -46,7 +44,6 @@
"error.user_not_found": "User not found", "error.user_not_found": "User not found",
"error.share_code_invalid": "Invalid share code", "error.share_code_invalid": "Invalid share code",
"error.share_code_expired": "Share code has expired", "error.share_code_expired": "Share code has expired",
"error.task_access_denied": "You don't have access to this task", "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.residence_access_denied": "You don't have access to this property",
"error.contractor_access_denied": "You don't have access to this contractor", "error.contractor_access_denied": "You don't have access to this contractor",
@@ -55,10 +52,8 @@
"error.cannot_remove_owner": "Cannot remove the property owner", "error.cannot_remove_owner": "Cannot remove the property owner",
"error.user_already_member": "User is already a member of this property", "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.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_cancelled": "Task is already cancelled",
"error.task_already_archived": "Task is already archived", "error.task_already_archived": "Task is already archived",
"error.failed_to_parse_form": "Failed to parse multipart form", "error.failed_to_parse_form": "Failed to parse multipart form",
"error.task_id_required": "task_id is required", "error.task_id_required": "task_id is required",
"error.invalid_task_id_value": "Invalid task_id", "error.invalid_task_id_value": "Invalid task_id",
@@ -67,14 +62,12 @@
"error.invalid_residence_id_value": "Invalid residence_id", "error.invalid_residence_id_value": "Invalid residence_id",
"error.title_required": "title is required", "error.title_required": "title is required",
"error.failed_to_upload_file": "Failed to upload file", "error.failed_to_upload_file": "Failed to upload file",
"message.logged_out": "Logged out successfully", "message.logged_out": "Logged out successfully",
"message.email_verified": "Email verified successfully", "message.email_verified": "Email verified successfully",
"message.verification_email_sent": "Verification email sent", "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.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.reset_code_verified": "Code verified successfully",
"message.password_reset_success": "Password reset successfully. Please log in with your new password.", "message.password_reset_success": "Password reset successfully. Please log in with your new password.",
"message.task_deleted": "Task deleted successfully", "message.task_deleted": "Task deleted successfully",
"message.task_in_progress": "Task marked as in progress", "message.task_in_progress": "Task marked as in progress",
"message.task_cancelled": "Task cancelled", "message.task_cancelled": "Task cancelled",
@@ -82,44 +75,34 @@
"message.task_archived": "Task archived", "message.task_archived": "Task archived",
"message.task_unarchived": "Task unarchived", "message.task_unarchived": "Task unarchived",
"message.completion_deleted": "Completion deleted successfully", "message.completion_deleted": "Completion deleted successfully",
"message.residence_deleted": "Residence deleted successfully", "message.residence_deleted": "Residence deleted successfully",
"message.user_removed": "User removed from residence", "message.user_removed": "User removed from residence",
"message.tasks_report_generated": "Tasks report generated successfully", "message.tasks_report_generated": "Tasks report generated successfully",
"message.tasks_report_sent": "Tasks report generated and sent to {{.Email}}", "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.tasks_report_email_failed": "Tasks report generated but email could not be sent",
"message.contractor_deleted": "Contractor deleted successfully", "message.contractor_deleted": "Contractor deleted successfully",
"message.document_deleted": "Document deleted successfully", "message.document_deleted": "Document deleted successfully",
"message.document_activated": "Document activated", "message.document_activated": "Document activated",
"message.document_deactivated": "Document deactivated", "message.document_deactivated": "Document deactivated",
"message.notification_marked_read": "Notification marked as read", "message.notification_marked_read": "Notification marked as read",
"message.all_notifications_marked_read": "All notifications marked as read", "message.all_notifications_marked_read": "All notifications marked as read",
"message.device_removed": "Device removed", "message.device_removed": "Device removed",
"message.subscription_upgraded": "Subscription upgraded successfully", "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_cancelled": "Subscription cancelled. You will retain Pro benefits until the end of your billing period.",
"message.subscription_restored": "Subscription restored successfully", "message.subscription_restored": "Subscription restored successfully",
"message.file_deleted": "File deleted successfully", "message.file_deleted": "File deleted successfully",
"message.static_data_refreshed": "Static data refreshed", "message.static_data_refreshed": "Static data refreshed",
"error.notification_not_found": "Notification not found", "error.notification_not_found": "Notification not found",
"error.invalid_platform": "Invalid platform", "error.invalid_platform": "Invalid platform",
"error.upgrade_trigger_not_found": "Upgrade trigger not found", "error.upgrade_trigger_not_found": "Upgrade trigger not found",
"error.receipt_data_required": "receipt_data is required for iOS", "error.receipt_data_required": "receipt_data is required for iOS",
"error.purchase_token_required": "purchase_token is required for Android", "error.purchase_token_required": "purchase_token is required for Android",
"error.no_file_provided": "No file provided", "error.no_file_provided": "No file provided",
"error.url_required": "File URL is required", "error.url_required": "File URL is required",
"error.file_access_denied": "You don't have access to this file", "error.file_access_denied": "You don't have access to this file",
"error.days_out_of_range": "Days parameter must be between 1 and 3650", "error.days_out_of_range": "Days parameter must be between 1 and 3650",
"error.platform_required": "Platform is required (ios or android)", "error.platform_required": "Platform is required (ios or android)",
"error.registration_id_required": "Registration ID is required", "error.registration_id_required": "Registration ID is required",
"error.failed_to_fetch_residence_types": "Failed to fetch residence types", "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_categories": "Failed to fetch task categories",
"error.failed_to_fetch_task_priorities": "Failed to fetch task priorities", "error.failed_to_fetch_task_priorities": "Failed to fetch task priorities",
@@ -129,7 +112,6 @@
"error.failed_to_fetch_templates": "Failed to fetch task templates", "error.failed_to_fetch_templates": "Failed to fetch task templates",
"error.failed_to_search_templates": "Failed to search task templates", "error.failed_to_search_templates": "Failed to search task templates",
"error.template_not_found": "Task template not found", "error.template_not_found": "Task template not found",
"push.task_due_soon.title": "Task Due Soon", "push.task_due_soon.title": "Task Due Soon",
"push.task_due_soon.body": "{{.TaskTitle}} is due {{.DueDate}}", "push.task_due_soon.body": "{{.TaskTitle}} is due {{.DueDate}}",
"push.task_overdue.title": "Overdue Task", "push.task_overdue.title": "Overdue Task",
@@ -140,19 +122,16 @@
"push.task_assigned.body": "You have been assigned to {{.TaskTitle}}", "push.task_assigned.body": "You have been assigned to {{.TaskTitle}}",
"push.residence_shared.title": "Property Shared", "push.residence_shared.title": "Property Shared",
"push.residence_shared.body": "{{.UserName}} shared {{.ResidenceName}} with you", "push.residence_shared.body": "{{.UserName}} shared {{.ResidenceName}} with you",
"email.welcome.subject": "Welcome to honeyDue!", "email.welcome.subject": "Welcome to honeyDue!",
"email.verification.subject": "Verify Your Email", "email.verification.subject": "Verify Your Email",
"email.password_reset.subject": "Password Reset Code", "email.password_reset.subject": "Password Reset Code",
"email.tasks_report.subject": "Tasks Report for {{.ResidenceName}}", "email.tasks_report.subject": "Tasks Report for {{.ResidenceName}}",
"lookup.residence_type.house": "House", "lookup.residence_type.house": "House",
"lookup.residence_type.apartment": "Apartment", "lookup.residence_type.apartment": "Apartment",
"lookup.residence_type.condo": "Condo", "lookup.residence_type.condo": "Condo",
"lookup.residence_type.townhouse": "Townhouse", "lookup.residence_type.townhouse": "Townhouse",
"lookup.residence_type.mobile_home": "Mobile Home", "lookup.residence_type.mobile_home": "Mobile Home",
"lookup.residence_type.other": "Other", "lookup.residence_type.other": "Other",
"lookup.task_category.plumbing": "Plumbing", "lookup.task_category.plumbing": "Plumbing",
"lookup.task_category.electrical": "Electrical", "lookup.task_category.electrical": "Electrical",
"lookup.task_category.hvac": "HVAC", "lookup.task_category.hvac": "HVAC",
@@ -165,18 +144,15 @@
"lookup.task_category.pest_control": "Pest Control", "lookup.task_category.pest_control": "Pest Control",
"lookup.task_category.seasonal": "Seasonal", "lookup.task_category.seasonal": "Seasonal",
"lookup.task_category.other": "Other", "lookup.task_category.other": "Other",
"lookup.task_priority.low": "Low", "lookup.task_priority.low": "Low",
"lookup.task_priority.medium": "Medium", "lookup.task_priority.medium": "Medium",
"lookup.task_priority.high": "High", "lookup.task_priority.high": "High",
"lookup.task_priority.urgent": "Urgent", "lookup.task_priority.urgent": "Urgent",
"lookup.task_status.pending": "Pending", "lookup.task_status.pending": "Pending",
"lookup.task_status.in_progress": "In Progress", "lookup.task_status.in_progress": "In Progress",
"lookup.task_status.completed": "Completed", "lookup.task_status.completed": "Completed",
"lookup.task_status.cancelled": "Cancelled", "lookup.task_status.cancelled": "Cancelled",
"lookup.task_status.archived": "Archived", "lookup.task_status.archived": "Archived",
"lookup.task_frequency.once": "Once", "lookup.task_frequency.once": "Once",
"lookup.task_frequency.daily": "Daily", "lookup.task_frequency.daily": "Daily",
"lookup.task_frequency.weekly": "Weekly", "lookup.task_frequency.weekly": "Weekly",
@@ -185,7 +161,6 @@
"lookup.task_frequency.quarterly": "Quarterly", "lookup.task_frequency.quarterly": "Quarterly",
"lookup.task_frequency.semiannually": "Every 6 Months", "lookup.task_frequency.semiannually": "Every 6 Months",
"lookup.task_frequency.annually": "Annually", "lookup.task_frequency.annually": "Annually",
"lookup.contractor_specialty.plumber": "Plumber", "lookup.contractor_specialty.plumber": "Plumber",
"lookup.contractor_specialty.electrician": "Electrician", "lookup.contractor_specialty.electrician": "Electrician",
"lookup.contractor_specialty.hvac_technician": "HVAC Technician", "lookup.contractor_specialty.hvac_technician": "HVAC Technician",
@@ -198,5 +173,86 @@
"lookup.contractor_specialty.cleaning": "Cleaning", "lookup.contractor_specialty.cleaning": "Cleaning",
"lookup.contractor_specialty.pool_service": "Pool Service", "lookup.contractor_specialty.pool_service": "Pool Service",
"lookup.contractor_specialty.general_contractor": "General Contractor", "lookup.contractor_specialty.general_contractor": "General Contractor",
"lookup.contractor_specialty.other": "Other" "lookup.contractor_specialty.other": "Other",
"suggestion.reason.has_pool": "Your home has a pool",
"suggestion.reason.has_sprinkler_system": "Your home has a sprinkler system",
"suggestion.reason.has_septic": "Your home has a septic system",
"suggestion.reason.has_fireplace": "Your home has a fireplace",
"suggestion.reason.has_garage": "Your home has a garage",
"suggestion.reason.has_basement": "Your home has a basement",
"suggestion.reason.has_attic": "Your home has an attic",
"suggestion.reason.heating_type": "Matches your heating system",
"suggestion.reason.cooling_type": "Matches your cooling system",
"suggestion.reason.water_heater_type": "Matches your water heater",
"suggestion.reason.roof_type": "Matches your roof",
"suggestion.reason.exterior_type": "Matches your exterior",
"suggestion.reason.flooring_primary": "Matches your flooring",
"suggestion.reason.landscaping_type": "Matches your landscaping",
"suggestion.reason.property_type": "Recommended for your property type",
"suggestion.reason.climate_region": "Recommended for your climate",
"lookup.residence_type.duplex": "Duplex",
"lookup.residence_type.vacation_home": "Vacation Home",
"lookup.task_category.general": "General",
"lookup.task_frequency.bi_weekly": "Bi-Weekly",
"lookup.task_frequency.semi_annually": "Semi-Annually",
"lookup.task_frequency.custom": "Custom",
"lookup.contractor_specialty.appliance_repair": "Appliance Repair",
"lookup.contractor_specialty.cleaner": "Cleaner",
"lookup.contractor_specialty.locksmith": "Locksmith",
"lookup.home_profile.gas_furnace": "Gas Furnace",
"lookup.home_profile.electric_furnace": "Electric Furnace",
"lookup.home_profile.heat_pump": "Heat Pump",
"lookup.home_profile.boiler": "Boiler",
"lookup.home_profile.radiant": "Radiant",
"lookup.home_profile.other": "Other",
"lookup.home_profile.central_ac": "Central AC",
"lookup.home_profile.window_ac": "Window AC",
"lookup.home_profile.evaporative": "Evaporative",
"lookup.home_profile.none": "None",
"lookup.home_profile.tank_gas": "Tank (Gas)",
"lookup.home_profile.tank_electric": "Tank (Electric)",
"lookup.home_profile.tankless_gas": "Tankless (Gas)",
"lookup.home_profile.tankless_electric": "Tankless (Electric)",
"lookup.home_profile.solar": "Solar",
"lookup.home_profile.asphalt_shingle": "Asphalt Shingle",
"lookup.home_profile.metal": "Metal",
"lookup.home_profile.tile": "Tile",
"lookup.home_profile.slate": "Slate",
"lookup.home_profile.wood_shake": "Wood Shake",
"lookup.home_profile.flat": "Flat",
"lookup.home_profile.brick": "Brick",
"lookup.home_profile.vinyl_siding": "Vinyl Siding",
"lookup.home_profile.wood_siding": "Wood Siding",
"lookup.home_profile.stucco": "Stucco",
"lookup.home_profile.stone": "Stone",
"lookup.home_profile.fiber_cement": "Fiber Cement",
"lookup.home_profile.hardwood": "Hardwood",
"lookup.home_profile.laminate": "Laminate",
"lookup.home_profile.carpet": "Carpet",
"lookup.home_profile.vinyl": "Vinyl",
"lookup.home_profile.concrete": "Concrete",
"lookup.home_profile.lawn": "Lawn",
"lookup.home_profile.desert": "Desert",
"lookup.home_profile.xeriscape": "Xeriscape",
"lookup.home_profile.garden": "Garden",
"lookup.home_profile.mixed": "Mixed",
"lookup.document_type.warranty": "Warranty",
"lookup.document_type.manual": "User Manual",
"lookup.document_type.receipt": "Receipt/Invoice",
"lookup.document_type.inspection": "Inspection Report",
"lookup.document_type.permit": "Permit",
"lookup.document_type.deed": "Deed/Title",
"lookup.document_type.insurance": "Insurance",
"lookup.document_type.contract": "Contract",
"lookup.document_type.photo": "Photo",
"lookup.document_type.other": "Other",
"lookup.document_category.appliance": "Appliance",
"lookup.document_category.hvac": "HVAC",
"lookup.document_category.plumbing": "Plumbing",
"lookup.document_category.electrical": "Electrical",
"lookup.document_category.roofing": "Roofing",
"lookup.document_category.structural": "Structural",
"lookup.document_category.landscaping": "Landscaping",
"lookup.document_category.general": "General",
"lookup.document_category.other": "Other"
} }
+95 -39
View File
@@ -25,7 +25,6 @@
"error.google_signin_not_configured": "El inicio de sesion con Google no esta configurado", "error.google_signin_not_configured": "El inicio de sesion con Google no esta configurado",
"error.google_signin_failed": "Error en el inicio de sesion con Google", "error.google_signin_failed": "Error en el inicio de sesion con Google",
"error.invalid_google_token": "Token de identidad de Google no valido", "error.invalid_google_token": "Token de identidad de Google no valido",
"error.invalid_task_id": "ID de tarea no valido", "error.invalid_task_id": "ID de tarea no valido",
"error.invalid_residence_id": "ID de propiedad no valido", "error.invalid_residence_id": "ID de propiedad no valido",
"error.invalid_contractor_id": "ID de contratista no valido", "error.invalid_contractor_id": "ID de contratista no valido",
@@ -34,7 +33,6 @@
"error.invalid_user_id": "ID de usuario no valido", "error.invalid_user_id": "ID de usuario no valido",
"error.invalid_notification_id": "ID de notificacion no valido", "error.invalid_notification_id": "ID de notificacion no valido",
"error.invalid_device_id": "ID de dispositivo no valido", "error.invalid_device_id": "ID de dispositivo no valido",
"error.task_not_found": "Tarea no encontrada", "error.task_not_found": "Tarea no encontrada",
"error.residence_not_found": "Propiedad no encontrada", "error.residence_not_found": "Propiedad no encontrada",
"error.contractor_not_found": "Contratista no encontrado", "error.contractor_not_found": "Contratista no encontrado",
@@ -43,7 +41,6 @@
"error.user_not_found": "Usuario no encontrado", "error.user_not_found": "Usuario no encontrado",
"error.share_code_invalid": "Codigo de compartir no valido", "error.share_code_invalid": "Codigo de compartir no valido",
"error.share_code_expired": "El codigo de compartir ha expirado", "error.share_code_expired": "El codigo de compartir ha expirado",
"error.task_access_denied": "No tienes acceso a esta tarea", "error.task_access_denied": "No tienes acceso a esta tarea",
"error.residence_access_denied": "No tienes acceso a esta propiedad", "error.residence_access_denied": "No tienes acceso a esta propiedad",
"error.contractor_access_denied": "No tienes acceso a este contratista", "error.contractor_access_denied": "No tienes acceso a este contratista",
@@ -52,10 +49,8 @@
"error.cannot_remove_owner": "No se puede eliminar al propietario de la propiedad", "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.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.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_cancelled": "La tarea ya esta cancelada",
"error.task_already_archived": "La tarea ya esta archivada", "error.task_already_archived": "La tarea ya esta archivada",
"error.failed_to_parse_form": "Error al analizar el formulario", "error.failed_to_parse_form": "Error al analizar el formulario",
"error.task_id_required": "Se requiere task_id", "error.task_id_required": "Se requiere task_id",
"error.invalid_task_id_value": "task_id no valido", "error.invalid_task_id_value": "task_id no valido",
@@ -64,14 +59,12 @@
"error.invalid_residence_id_value": "residence_id no valido", "error.invalid_residence_id_value": "residence_id no valido",
"error.title_required": "Se requiere el titulo", "error.title_required": "Se requiere el titulo",
"error.failed_to_upload_file": "Error al subir el archivo", "error.failed_to_upload_file": "Error al subir el archivo",
"message.logged_out": "Sesion cerrada correctamente", "message.logged_out": "Sesion cerrada correctamente",
"message.email_verified": "Correo electronico verificado correctamente", "message.email_verified": "Correo electronico verificado correctamente",
"message.verification_email_sent": "Correo de verificacion enviado", "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.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.reset_code_verified": "Codigo verificado correctamente",
"message.password_reset_success": "Contrasena restablecida correctamente. Por favor, inicia sesion con tu nueva contrasena.", "message.password_reset_success": "Contrasena restablecida correctamente. Por favor, inicia sesion con tu nueva contrasena.",
"message.task_deleted": "Tarea eliminada correctamente", "message.task_deleted": "Tarea eliminada correctamente",
"message.task_in_progress": "Tarea marcada como en progreso", "message.task_in_progress": "Tarea marcada como en progreso",
"message.task_cancelled": "Tarea cancelada", "message.task_cancelled": "Tarea cancelada",
@@ -79,46 +72,35 @@
"message.task_archived": "Tarea archivada", "message.task_archived": "Tarea archivada",
"message.task_unarchived": "Tarea desarchivada", "message.task_unarchived": "Tarea desarchivada",
"message.completion_deleted": "Finalizacion eliminada correctamente", "message.completion_deleted": "Finalizacion eliminada correctamente",
"message.residence_deleted": "Propiedad eliminada correctamente", "message.residence_deleted": "Propiedad eliminada correctamente",
"message.user_removed": "Usuario eliminado de la propiedad", "message.user_removed": "Usuario eliminado de la propiedad",
"message.tasks_report_generated": "Informe de tareas generado correctamente", "message.tasks_report_generated": "Informe de tareas generado correctamente",
"message.tasks_report_sent": "Informe de tareas generado y enviado a {{.Email}}", "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.tasks_report_email_failed": "Informe de tareas generado pero no se pudo enviar el correo",
"message.contractor_deleted": "Contratista eliminado correctamente", "message.contractor_deleted": "Contratista eliminado correctamente",
"message.document_deleted": "Documento eliminado correctamente", "message.document_deleted": "Documento eliminado correctamente",
"message.document_activated": "Documento activado", "message.document_activated": "Documento activado",
"message.document_deactivated": "Documento desactivado", "message.document_deactivated": "Documento desactivado",
"message.notification_marked_read": "Notificación marcada como leída", "message.notification_marked_read": "Notificación marcada como leída",
"message.all_notifications_marked_read": "Todas las notificaciones marcadas como leídas", "message.all_notifications_marked_read": "Todas las notificaciones marcadas como leídas",
"message.device_removed": "Dispositivo eliminado", "message.device_removed": "Dispositivo eliminado",
"message.subscription_upgraded": "Suscripción actualizada correctamente", "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_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.subscription_restored": "Suscripción restaurada correctamente",
"message.file_deleted": "Archivo eliminado correctamente", "message.file_deleted": "Archivo eliminado correctamente",
"message.static_data_refreshed": "Datos estáticos actualizados", "message.static_data_refreshed": "Datos estáticos actualizados",
"error.notification_not_found": "Notificación no encontrada", "error.notification_not_found": "Notificación no encontrada",
"error.invalid_platform": "Plataforma no válida", "error.invalid_platform": "Plataforma no válida",
"error.upgrade_trigger_not_found": "Trigger de actualización no encontrado", "error.upgrade_trigger_not_found": "Trigger de actualización no encontrado",
"error.receipt_data_required": "Se requiere receipt_data para iOS", "error.receipt_data_required": "Se requiere receipt_data para iOS",
"error.purchase_token_required": "Se requiere purchase_token para Android", "error.purchase_token_required": "Se requiere purchase_token para Android",
"error.no_file_provided": "No se proporcionó ningún archivo", "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_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_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_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_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_task_statuses": "Error al obtener los estados de tareas",
"error.failed_to_fetch_contractor_specialties": "Error al obtener las especialidades de contratistas", "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.title": "Tarea Proxima a Vencer",
"push.task_due_soon.body": "{{.TaskTitle}} vence {{.DueDate}}", "push.task_due_soon.body": "{{.TaskTitle}} vence {{.DueDate}}",
"push.task_overdue.title": "Tarea Vencida", "push.task_overdue.title": "Tarea Vencida",
@@ -129,44 +111,38 @@
"push.task_assigned.body": "Se te ha asignado {{.TaskTitle}}", "push.task_assigned.body": "Se te ha asignado {{.TaskTitle}}",
"push.residence_shared.title": "Propiedad Compartida", "push.residence_shared.title": "Propiedad Compartida",
"push.residence_shared.body": "{{.UserName}} compartio {{.ResidenceName}} contigo", "push.residence_shared.body": "{{.UserName}} compartio {{.ResidenceName}} contigo",
"email.welcome.subject": "Bienvenido a honeyDue!", "email.welcome.subject": "Bienvenido a honeyDue!",
"email.verification.subject": "Verifica Tu Correo Electronico", "email.verification.subject": "Verifica Tu Correo Electronico",
"email.password_reset.subject": "Codigo de Restablecimiento de Contrasena", "email.password_reset.subject": "Codigo de Restablecimiento de Contrasena",
"email.tasks_report.subject": "Informe de Tareas para {{.ResidenceName}}", "email.tasks_report.subject": "Informe de Tareas para {{.ResidenceName}}",
"lookup.residence_type.house": "Casa", "lookup.residence_type.house": "Casa",
"lookup.residence_type.apartment": "Apartamento", "lookup.residence_type.apartment": "Apartamento",
"lookup.residence_type.condo": "Condominio", "lookup.residence_type.condo": "Condominio",
"lookup.residence_type.townhouse": "Casa Adosada", "lookup.residence_type.townhouse": "Casa adosada",
"lookup.residence_type.mobile_home": "Casa Movil", "lookup.residence_type.mobile_home": "Casa vil",
"lookup.residence_type.other": "Otro", "lookup.residence_type.other": "Otro",
"lookup.task_category.plumbing": "Fontanería",
"lookup.task_category.plumbing": "Plomeria", "lookup.task_category.electrical": "Eléctrico",
"lookup.task_category.electrical": "Electricidad", "lookup.task_category.hvac": "Climatización",
"lookup.task_category.hvac": "Climatizacion", "lookup.task_category.appliances": "Electrodomésticos",
"lookup.task_category.appliances": "Electrodomesticos",
"lookup.task_category.exterior": "Exterior", "lookup.task_category.exterior": "Exterior",
"lookup.task_category.interior": "Interior", "lookup.task_category.interior": "Interior",
"lookup.task_category.landscaping": "Jardineria", "lookup.task_category.landscaping": "Jardineria",
"lookup.task_category.safety": "Seguridad", "lookup.task_category.safety": "Seguridad",
"lookup.task_category.cleaning": "Limpieza", "lookup.task_category.cleaning": "Limpieza",
"lookup.task_category.pest_control": "Control de Plagas", "lookup.task_category.pest_control": "Control de plagas",
"lookup.task_category.seasonal": "Estacional", "lookup.task_category.seasonal": "Estacional",
"lookup.task_category.other": "Otro", "lookup.task_category.other": "Otro",
"lookup.task_priority.low": "Baja", "lookup.task_priority.low": "Baja",
"lookup.task_priority.medium": "Media", "lookup.task_priority.medium": "Media",
"lookup.task_priority.high": "Alta", "lookup.task_priority.high": "Alta",
"lookup.task_priority.urgent": "Urgente", "lookup.task_priority.urgent": "Urgente",
"lookup.task_status.pending": "Pendiente", "lookup.task_status.pending": "Pendiente",
"lookup.task_status.in_progress": "En Progreso", "lookup.task_status.in_progress": "En Progreso",
"lookup.task_status.completed": "Completada", "lookup.task_status.completed": "Completada",
"lookup.task_status.cancelled": "Cancelada", "lookup.task_status.cancelled": "Cancelada",
"lookup.task_status.archived": "Archivada", "lookup.task_status.archived": "Archivada",
"lookup.task_frequency.once": "Una vez",
"lookup.task_frequency.once": "Una Vez",
"lookup.task_frequency.daily": "Diario", "lookup.task_frequency.daily": "Diario",
"lookup.task_frequency.weekly": "Semanal", "lookup.task_frequency.weekly": "Semanal",
"lookup.task_frequency.biweekly": "Cada 2 Semanas", "lookup.task_frequency.biweekly": "Cada 2 Semanas",
@@ -174,18 +150,98 @@
"lookup.task_frequency.quarterly": "Trimestral", "lookup.task_frequency.quarterly": "Trimestral",
"lookup.task_frequency.semiannually": "Cada 6 Meses", "lookup.task_frequency.semiannually": "Cada 6 Meses",
"lookup.task_frequency.annually": "Anual", "lookup.task_frequency.annually": "Anual",
"lookup.contractor_specialty.plumber": "Fontanero",
"lookup.contractor_specialty.plumber": "Plomero",
"lookup.contractor_specialty.electrician": "Electricista", "lookup.contractor_specialty.electrician": "Electricista",
"lookup.contractor_specialty.hvac_technician": "Tecnico de Climatizacion", "lookup.contractor_specialty.hvac_technician": "Técnico de climatización",
"lookup.contractor_specialty.handyman": "Manitas", "lookup.contractor_specialty.handyman": "Manitas",
"lookup.contractor_specialty.landscaper": "Jardinero", "lookup.contractor_specialty.landscaper": "Jardinero",
"lookup.contractor_specialty.roofer": "Techador", "lookup.contractor_specialty.roofer": "Techador",
"lookup.contractor_specialty.painter": "Pintor", "lookup.contractor_specialty.painter": "Pintor",
"lookup.contractor_specialty.carpenter": "Carpintero", "lookup.contractor_specialty.carpenter": "Carpintero",
"lookup.contractor_specialty.pest_control": "Control de Plagas", "lookup.contractor_specialty.pest_control": "Control de plagas",
"lookup.contractor_specialty.cleaning": "Limpieza", "lookup.contractor_specialty.cleaning": "Limpieza",
"lookup.contractor_specialty.pool_service": "Servicio de Piscina", "lookup.contractor_specialty.pool_service": "Servicio de piscina",
"lookup.contractor_specialty.general_contractor": "Contratista General", "lookup.contractor_specialty.general_contractor": "Contratista general",
"lookup.contractor_specialty.other": "Otro" "lookup.contractor_specialty.other": "Otro",
"suggestion.reason.has_pool": "Tu casa tiene piscina",
"suggestion.reason.has_sprinkler_system": "Tu casa tiene sistema de riego",
"suggestion.reason.has_septic": "Tu casa tiene fosa séptica",
"suggestion.reason.has_fireplace": "Tu casa tiene chimenea",
"suggestion.reason.has_garage": "Tu casa tiene garaje",
"suggestion.reason.has_basement": "Tu casa tiene sótano",
"suggestion.reason.has_attic": "Tu casa tiene ático",
"suggestion.reason.heating_type": "Coincide con tu sistema de calefacción",
"suggestion.reason.cooling_type": "Coincide con tu sistema de refrigeración",
"suggestion.reason.water_heater_type": "Coincide con tu calentador de agua",
"suggestion.reason.roof_type": "Coincide con tu tejado",
"suggestion.reason.exterior_type": "Coincide con tu exterior",
"suggestion.reason.flooring_primary": "Coincide con tu suelo",
"suggestion.reason.landscaping_type": "Coincide con tu jardín",
"suggestion.reason.property_type": "Recomendado para tu tipo de propiedad",
"suggestion.reason.climate_region": "Recomendado para tu clima",
"lookup.residence_type.duplex": "Dúplex",
"lookup.residence_type.vacation_home": "Casa de vacaciones",
"lookup.task_category.general": "General",
"lookup.task_frequency.bi_weekly": "Quincenal",
"lookup.task_frequency.semi_annually": "Semestral",
"lookup.task_frequency.custom": "Personalizado",
"lookup.contractor_specialty.appliance_repair": "Reparación de electrodomésticos",
"lookup.contractor_specialty.cleaner": "Limpiador",
"lookup.contractor_specialty.locksmith": "Cerrajero",
"lookup.home_profile.gas_furnace": "Calefactor de gas",
"lookup.home_profile.electric_furnace": "Calefactor eléctrico",
"lookup.home_profile.heat_pump": "Bomba de calor",
"lookup.home_profile.boiler": "Caldera",
"lookup.home_profile.radiant": "Radiante",
"lookup.home_profile.other": "Otro",
"lookup.home_profile.central_ac": "AC central",
"lookup.home_profile.window_ac": "AC de ventana",
"lookup.home_profile.evaporative": "Evaporativo",
"lookup.home_profile.none": "Ninguno",
"lookup.home_profile.tank_gas": "Tanque (gas)",
"lookup.home_profile.tank_electric": "Tanque (eléctrico)",
"lookup.home_profile.tankless_gas": "Sin tanque (gas)",
"lookup.home_profile.tankless_electric": "Sin tanque (eléctrico)",
"lookup.home_profile.solar": "Solar",
"lookup.home_profile.asphalt_shingle": "Teja asfáltica",
"lookup.home_profile.metal": "Metal",
"lookup.home_profile.tile": "Teja",
"lookup.home_profile.slate": "Pizarra",
"lookup.home_profile.wood_shake": "Tablilla de madera",
"lookup.home_profile.flat": "Plano",
"lookup.home_profile.brick": "Ladrillo",
"lookup.home_profile.vinyl_siding": "Revestimiento de vinilo",
"lookup.home_profile.wood_siding": "Revestimiento de madera",
"lookup.home_profile.stucco": "Estuco",
"lookup.home_profile.stone": "Piedra",
"lookup.home_profile.fiber_cement": "Fibrocemento",
"lookup.home_profile.hardwood": "Madera dura",
"lookup.home_profile.laminate": "Laminado",
"lookup.home_profile.carpet": "Alfombra",
"lookup.home_profile.vinyl": "Vinilo",
"lookup.home_profile.concrete": "Hormigón",
"lookup.home_profile.lawn": "Césped",
"lookup.home_profile.desert": "Desierto",
"lookup.home_profile.xeriscape": "Xerojardinería",
"lookup.home_profile.garden": "Jardín",
"lookup.home_profile.mixed": "Mixto",
"lookup.document_type.warranty": "Garantía",
"lookup.document_type.manual": "Manual de usuario",
"lookup.document_type.receipt": "Recibo/Factura",
"lookup.document_type.inspection": "Informe de inspección",
"lookup.document_type.permit": "Permiso",
"lookup.document_type.deed": "Escritura/Título",
"lookup.document_type.insurance": "Seguro",
"lookup.document_type.contract": "Contrato",
"lookup.document_type.photo": "Foto",
"lookup.document_type.other": "Otro",
"lookup.document_category.appliance": "Electrodoméstico",
"lookup.document_category.hvac": "Climatización",
"lookup.document_category.plumbing": "Fontanería",
"lookup.document_category.electrical": "Eléctrico",
"lookup.document_category.roofing": "Tejado",
"lookup.document_category.structural": "Estructural",
"lookup.document_category.landscaping": "Jardinería",
"lookup.document_category.general": "General",
"lookup.document_category.other": "Otro"
} }
+98 -42
View File
@@ -25,7 +25,6 @@
"error.google_signin_not_configured": "La connexion Google n'est pas configuree", "error.google_signin_not_configured": "La connexion Google n'est pas configuree",
"error.google_signin_failed": "Echec de la connexion Google", "error.google_signin_failed": "Echec de la connexion Google",
"error.invalid_google_token": "Jeton d'identite Google non valide", "error.invalid_google_token": "Jeton d'identite Google non valide",
"error.invalid_task_id": "ID de tache non valide", "error.invalid_task_id": "ID de tache non valide",
"error.invalid_residence_id": "ID de propriete non valide", "error.invalid_residence_id": "ID de propriete non valide",
"error.invalid_contractor_id": "ID de prestataire non valide", "error.invalid_contractor_id": "ID de prestataire non valide",
@@ -34,7 +33,6 @@
"error.invalid_user_id": "ID d'utilisateur non valide", "error.invalid_user_id": "ID d'utilisateur non valide",
"error.invalid_notification_id": "ID de notification non valide", "error.invalid_notification_id": "ID de notification non valide",
"error.invalid_device_id": "ID d'appareil non valide", "error.invalid_device_id": "ID d'appareil non valide",
"error.task_not_found": "Tache non trouvee", "error.task_not_found": "Tache non trouvee",
"error.residence_not_found": "Propriete non trouvee", "error.residence_not_found": "Propriete non trouvee",
"error.contractor_not_found": "Prestataire non trouve", "error.contractor_not_found": "Prestataire non trouve",
@@ -43,7 +41,6 @@
"error.user_not_found": "Utilisateur non trouve", "error.user_not_found": "Utilisateur non trouve",
"error.share_code_invalid": "Code de partage non valide", "error.share_code_invalid": "Code de partage non valide",
"error.share_code_expired": "Le code de partage a expire", "error.share_code_expired": "Le code de partage a expire",
"error.task_access_denied": "Vous n'avez pas acces a cette tache", "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.residence_access_denied": "Vous n'avez pas acces a cette propriete",
"error.contractor_access_denied": "Vous n'avez pas acces a ce prestataire", "error.contractor_access_denied": "Vous n'avez pas acces a ce prestataire",
@@ -52,10 +49,8 @@
"error.cannot_remove_owner": "Impossible de retirer le proprietaire", "error.cannot_remove_owner": "Impossible de retirer le proprietaire",
"error.user_already_member": "L'utilisateur est deja membre de cette propriete", "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.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_cancelled": "La tache est deja annulee",
"error.task_already_archived": "La tache est deja archivee", "error.task_already_archived": "La tache est deja archivee",
"error.failed_to_parse_form": "Echec de l'analyse du formulaire", "error.failed_to_parse_form": "Echec de l'analyse du formulaire",
"error.task_id_required": "task_id est requis", "error.task_id_required": "task_id est requis",
"error.invalid_task_id_value": "task_id non valide", "error.invalid_task_id_value": "task_id non valide",
@@ -64,14 +59,12 @@
"error.invalid_residence_id_value": "residence_id non valide", "error.invalid_residence_id_value": "residence_id non valide",
"error.title_required": "Le titre est requis", "error.title_required": "Le titre est requis",
"error.failed_to_upload_file": "Echec du telechargement du fichier", "error.failed_to_upload_file": "Echec du telechargement du fichier",
"message.logged_out": "Deconnexion reussie", "message.logged_out": "Deconnexion reussie",
"message.email_verified": "Email verifie avec succes", "message.email_verified": "Email verifie avec succes",
"message.verification_email_sent": "Email de verification envoye", "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.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.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.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_deleted": "Tache supprimee avec succes",
"message.task_in_progress": "Tache marquee comme en cours", "message.task_in_progress": "Tache marquee comme en cours",
"message.task_cancelled": "Tache annulee", "message.task_cancelled": "Tache annulee",
@@ -79,46 +72,35 @@
"message.task_archived": "Tache archivee", "message.task_archived": "Tache archivee",
"message.task_unarchived": "Tache desarchivee", "message.task_unarchived": "Tache desarchivee",
"message.completion_deleted": "Completion supprimee avec succes", "message.completion_deleted": "Completion supprimee avec succes",
"message.residence_deleted": "Propriete supprimee avec succes", "message.residence_deleted": "Propriete supprimee avec succes",
"message.user_removed": "Utilisateur retire de la propriete", "message.user_removed": "Utilisateur retire de la propriete",
"message.tasks_report_generated": "Rapport de taches genere avec succes", "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_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.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.contractor_deleted": "Prestataire supprime avec succes",
"message.document_deleted": "Document supprime avec succes", "message.document_deleted": "Document supprime avec succes",
"message.document_activated": "Document active", "message.document_activated": "Document active",
"message.document_deactivated": "Document desactive", "message.document_deactivated": "Document desactive",
"message.notification_marked_read": "Notification marquée comme lue", "message.notification_marked_read": "Notification marquée comme lue",
"message.all_notifications_marked_read": "Toutes les notifications marquées comme lues", "message.all_notifications_marked_read": "Toutes les notifications marquées comme lues",
"message.device_removed": "Appareil supprimé", "message.device_removed": "Appareil supprimé",
"message.subscription_upgraded": "Abonnement mis à niveau avec succès", "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_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.subscription_restored": "Abonnement restauré avec succès",
"message.file_deleted": "Fichier supprimé avec succès", "message.file_deleted": "Fichier supprimé avec succès",
"message.static_data_refreshed": "Données statiques actualisées", "message.static_data_refreshed": "Données statiques actualisées",
"error.notification_not_found": "Notification non trouvée", "error.notification_not_found": "Notification non trouvée",
"error.invalid_platform": "Plateforme non valide", "error.invalid_platform": "Plateforme non valide",
"error.upgrade_trigger_not_found": "Déclencheur de mise à niveau non trouvé", "error.upgrade_trigger_not_found": "Déclencheur de mise à niveau non trouvé",
"error.receipt_data_required": "receipt_data est requis pour iOS", "error.receipt_data_required": "receipt_data est requis pour iOS",
"error.purchase_token_required": "purchase_token est requis pour Android", "error.purchase_token_required": "purchase_token est requis pour Android",
"error.no_file_provided": "Aucun fichier fourni", "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_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_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_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_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_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", "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.title": "Tache Bientot Due",
"push.task_due_soon.body": "{{.TaskTitle}} est due le {{.DueDate}}", "push.task_due_soon.body": "{{.TaskTitle}} est due le {{.DueDate}}",
"push.task_overdue.title": "Tache en Retard", "push.task_overdue.title": "Tache en Retard",
@@ -129,44 +111,38 @@
"push.task_assigned.body": "{{.TaskTitle}} vous a ete assignee", "push.task_assigned.body": "{{.TaskTitle}} vous a ete assignee",
"push.residence_shared.title": "Propriete Partagee", "push.residence_shared.title": "Propriete Partagee",
"push.residence_shared.body": "{{.UserName}} a partage {{.ResidenceName}} avec vous", "push.residence_shared.body": "{{.UserName}} a partage {{.ResidenceName}} avec vous",
"email.welcome.subject": "Bienvenue sur honeyDue !", "email.welcome.subject": "Bienvenue sur honeyDue !",
"email.verification.subject": "Verifiez Votre Email", "email.verification.subject": "Verifiez Votre Email",
"email.password_reset.subject": "Code de Reinitialisation de Mot de Passe", "email.password_reset.subject": "Code de Reinitialisation de Mot de Passe",
"email.tasks_report.subject": "Rapport de Taches pour {{.ResidenceName}}", "email.tasks_report.subject": "Rapport de Taches pour {{.ResidenceName}}",
"lookup.residence_type.house": "Maison", "lookup.residence_type.house": "Maison",
"lookup.residence_type.apartment": "Appartement", "lookup.residence_type.apartment": "Appartement",
"lookup.residence_type.condo": "Copropriete", "lookup.residence_type.condo": "Copropriété",
"lookup.residence_type.townhouse": "Maison de Ville", "lookup.residence_type.townhouse": "Maison de ville",
"lookup.residence_type.mobile_home": "Mobil-home", "lookup.residence_type.mobile_home": "Maison mobile",
"lookup.residence_type.other": "Autre", "lookup.residence_type.other": "Autre",
"lookup.task_category.plumbing": "Plomberie", "lookup.task_category.plumbing": "Plomberie",
"lookup.task_category.electrical": "Electricite", "lookup.task_category.electrical": "Électricité",
"lookup.task_category.hvac": "Climatisation", "lookup.task_category.hvac": "CVC",
"lookup.task_category.appliances": "Electromenager", "lookup.task_category.appliances": "Électroménager",
"lookup.task_category.exterior": "Exterieur", "lookup.task_category.exterior": "Extérieur",
"lookup.task_category.interior": "Interieur", "lookup.task_category.interior": "Intérieur",
"lookup.task_category.landscaping": "Jardinage", "lookup.task_category.landscaping": "Jardinage",
"lookup.task_category.safety": "Securite", "lookup.task_category.safety": "Sécurité",
"lookup.task_category.cleaning": "Nettoyage", "lookup.task_category.cleaning": "Nettoyage",
"lookup.task_category.pest_control": "Lutte Antiparasitaire", "lookup.task_category.pest_control": "Lutte antiparasitaire",
"lookup.task_category.seasonal": "Saisonnier", "lookup.task_category.seasonal": "Saisonnier",
"lookup.task_category.other": "Autre", "lookup.task_category.other": "Autre",
"lookup.task_priority.low": "Basse", "lookup.task_priority.low": "Basse",
"lookup.task_priority.medium": "Moyenne", "lookup.task_priority.medium": "Moyenne",
"lookup.task_priority.high": "Haute", "lookup.task_priority.high": "Haute",
"lookup.task_priority.urgent": "Urgente", "lookup.task_priority.urgent": "Urgente",
"lookup.task_status.pending": "En Attente", "lookup.task_status.pending": "En Attente",
"lookup.task_status.in_progress": "En Cours", "lookup.task_status.in_progress": "En Cours",
"lookup.task_status.completed": "Terminee", "lookup.task_status.completed": "Terminee",
"lookup.task_status.cancelled": "Annulee", "lookup.task_status.cancelled": "Annulee",
"lookup.task_status.archived": "Archivee", "lookup.task_status.archived": "Archivee",
"lookup.task_frequency.once": "Une fois",
"lookup.task_frequency.once": "Une Fois",
"lookup.task_frequency.daily": "Quotidien", "lookup.task_frequency.daily": "Quotidien",
"lookup.task_frequency.weekly": "Hebdomadaire", "lookup.task_frequency.weekly": "Hebdomadaire",
"lookup.task_frequency.biweekly": "Toutes les 2 Semaines", "lookup.task_frequency.biweekly": "Toutes les 2 Semaines",
@@ -174,18 +150,98 @@
"lookup.task_frequency.quarterly": "Trimestriel", "lookup.task_frequency.quarterly": "Trimestriel",
"lookup.task_frequency.semiannually": "Tous les 6 Mois", "lookup.task_frequency.semiannually": "Tous les 6 Mois",
"lookup.task_frequency.annually": "Annuel", "lookup.task_frequency.annually": "Annuel",
"lookup.contractor_specialty.plumber": "Plombier", "lookup.contractor_specialty.plumber": "Plombier",
"lookup.contractor_specialty.electrician": "Electricien", "lookup.contractor_specialty.electrician": "Électricien",
"lookup.contractor_specialty.hvac_technician": "Technicien CVC", "lookup.contractor_specialty.hvac_technician": "Technicien CVC",
"lookup.contractor_specialty.handyman": "Bricoleur", "lookup.contractor_specialty.handyman": "Bricoleur",
"lookup.contractor_specialty.landscaper": "Paysagiste", "lookup.contractor_specialty.landscaper": "Paysagiste",
"lookup.contractor_specialty.roofer": "Couvreur", "lookup.contractor_specialty.roofer": "Couvreur",
"lookup.contractor_specialty.painter": "Peintre", "lookup.contractor_specialty.painter": "Peintre",
"lookup.contractor_specialty.carpenter": "Menuisier", "lookup.contractor_specialty.carpenter": "Charpentier",
"lookup.contractor_specialty.pest_control": "Desinsectisation", "lookup.contractor_specialty.pest_control": "Lutte antiparasitaire",
"lookup.contractor_specialty.cleaning": "Nettoyage", "lookup.contractor_specialty.cleaning": "Nettoyage",
"lookup.contractor_specialty.pool_service": "Service Piscine", "lookup.contractor_specialty.pool_service": "Service de piscine",
"lookup.contractor_specialty.general_contractor": "Entrepreneur General", "lookup.contractor_specialty.general_contractor": "Entrepreneur général",
"lookup.contractor_specialty.other": "Autre" "lookup.contractor_specialty.other": "Autre",
"suggestion.reason.has_pool": "Votre logement a une piscine",
"suggestion.reason.has_sprinkler_system": "Votre logement a un système d'arrosage",
"suggestion.reason.has_septic": "Votre logement a une fosse septique",
"suggestion.reason.has_fireplace": "Votre logement a une cheminée",
"suggestion.reason.has_garage": "Votre logement a un garage",
"suggestion.reason.has_basement": "Votre logement a un sous-sol",
"suggestion.reason.has_attic": "Votre logement a des combles",
"suggestion.reason.heating_type": "Correspond à votre système de chauffage",
"suggestion.reason.cooling_type": "Correspond à votre système de climatisation",
"suggestion.reason.water_heater_type": "Correspond à votre chauffe-eau",
"suggestion.reason.roof_type": "Correspond à votre toiture",
"suggestion.reason.exterior_type": "Correspond à votre extérieur",
"suggestion.reason.flooring_primary": "Correspond à votre revêtement de sol",
"suggestion.reason.landscaping_type": "Correspond à votre aménagement paysager",
"suggestion.reason.property_type": "Recommandé pour votre type de logement",
"suggestion.reason.climate_region": "Recommandé pour votre climat",
"lookup.residence_type.duplex": "Duplex",
"lookup.residence_type.vacation_home": "Maison de vacances",
"lookup.task_category.general": "Général",
"lookup.task_frequency.bi_weekly": "Bimensuel",
"lookup.task_frequency.semi_annually": "Semestriel",
"lookup.task_frequency.custom": "Personnalisé",
"lookup.contractor_specialty.appliance_repair": "Réparation d'électroménager",
"lookup.contractor_specialty.cleaner": "Agent de nettoyage",
"lookup.contractor_specialty.locksmith": "Serrurier",
"lookup.home_profile.gas_furnace": "Fournaise au gaz",
"lookup.home_profile.electric_furnace": "Fournaise électrique",
"lookup.home_profile.heat_pump": "Pompe à chaleur",
"lookup.home_profile.boiler": "Chaudière",
"lookup.home_profile.radiant": "Rayonnant",
"lookup.home_profile.other": "Autre",
"lookup.home_profile.central_ac": "Climatisation centrale",
"lookup.home_profile.window_ac": "Climatiseur de fenêtre",
"lookup.home_profile.evaporative": "Évaporatif",
"lookup.home_profile.none": "Aucun",
"lookup.home_profile.tank_gas": "Réservoir (gaz)",
"lookup.home_profile.tank_electric": "Réservoir (électrique)",
"lookup.home_profile.tankless_gas": "Sans réservoir (gaz)",
"lookup.home_profile.tankless_electric": "Sans réservoir (électrique)",
"lookup.home_profile.solar": "Solaire",
"lookup.home_profile.asphalt_shingle": "Bardeau d'asphalte",
"lookup.home_profile.metal": "Métal",
"lookup.home_profile.tile": "Tuile",
"lookup.home_profile.slate": "Ardoise",
"lookup.home_profile.wood_shake": "Bardeau de bois",
"lookup.home_profile.flat": "Plat",
"lookup.home_profile.brick": "Brique",
"lookup.home_profile.vinyl_siding": "Revêtement vinyle",
"lookup.home_profile.wood_siding": "Revêtement bois",
"lookup.home_profile.stucco": "Stuc",
"lookup.home_profile.stone": "Pierre",
"lookup.home_profile.fiber_cement": "Fibrociment",
"lookup.home_profile.hardwood": "Bois franc",
"lookup.home_profile.laminate": "Stratifié",
"lookup.home_profile.carpet": "Moquette",
"lookup.home_profile.vinyl": "Vinyle",
"lookup.home_profile.concrete": "Béton",
"lookup.home_profile.lawn": "Pelouse",
"lookup.home_profile.desert": "Désert",
"lookup.home_profile.xeriscape": "Xéropaysagisme",
"lookup.home_profile.garden": "Jardin",
"lookup.home_profile.mixed": "Mixte",
"lookup.document_type.warranty": "Garantie",
"lookup.document_type.manual": "Manuel d'utilisation",
"lookup.document_type.receipt": "Reçu/Facture",
"lookup.document_type.inspection": "Rapport d'inspection",
"lookup.document_type.permit": "Permis",
"lookup.document_type.deed": "Acte/Titre",
"lookup.document_type.insurance": "Assurance",
"lookup.document_type.contract": "Contrat",
"lookup.document_type.photo": "Photo",
"lookup.document_type.other": "Autre",
"lookup.document_category.appliance": "Électroménager",
"lookup.document_category.hvac": "CVC",
"lookup.document_category.plumbing": "Plomberie",
"lookup.document_category.electrical": "Électricité",
"lookup.document_category.roofing": "Toiture",
"lookup.document_category.structural": "Structure",
"lookup.document_category.landscaping": "Aménagement paysager",
"lookup.document_category.general": "Général",
"lookup.document_category.other": "Autre"
} }
+92 -36
View File
@@ -25,7 +25,6 @@
"error.google_signin_not_configured": "L'accesso con Google non è configurato", "error.google_signin_not_configured": "L'accesso con Google non è configurato",
"error.google_signin_failed": "Accesso con Google fallito", "error.google_signin_failed": "Accesso con Google fallito",
"error.invalid_google_token": "Token di identità Google non valido", "error.invalid_google_token": "Token di identità Google non valido",
"error.invalid_task_id": "ID attività non valido", "error.invalid_task_id": "ID attività non valido",
"error.invalid_residence_id": "ID immobile non valido", "error.invalid_residence_id": "ID immobile non valido",
"error.invalid_contractor_id": "ID fornitore non valido", "error.invalid_contractor_id": "ID fornitore non valido",
@@ -34,7 +33,6 @@
"error.invalid_user_id": "ID utente non valido", "error.invalid_user_id": "ID utente non valido",
"error.invalid_notification_id": "ID notifica non valido", "error.invalid_notification_id": "ID notifica non valido",
"error.invalid_device_id": "ID dispositivo non valido", "error.invalid_device_id": "ID dispositivo non valido",
"error.task_not_found": "Attività non trovata", "error.task_not_found": "Attività non trovata",
"error.residence_not_found": "Immobile non trovato", "error.residence_not_found": "Immobile non trovato",
"error.contractor_not_found": "Fornitore non trovato", "error.contractor_not_found": "Fornitore non trovato",
@@ -43,7 +41,6 @@
"error.user_not_found": "Utente non trovato", "error.user_not_found": "Utente non trovato",
"error.share_code_invalid": "Codice di condivisione non valido", "error.share_code_invalid": "Codice di condivisione non valido",
"error.share_code_expired": "Il codice di condivisione è scaduto", "error.share_code_expired": "Il codice di condivisione è scaduto",
"error.task_access_denied": "Non hai accesso a questa attività", "error.task_access_denied": "Non hai accesso a questa attività",
"error.residence_access_denied": "Non hai accesso a questo immobile", "error.residence_access_denied": "Non hai accesso a questo immobile",
"error.contractor_access_denied": "Non hai accesso a questo fornitore", "error.contractor_access_denied": "Non hai accesso a questo fornitore",
@@ -52,10 +49,8 @@
"error.cannot_remove_owner": "Impossibile rimuovere il proprietario dell'immobile", "error.cannot_remove_owner": "Impossibile rimuovere il proprietario dell'immobile",
"error.user_already_member": "L'utente è già membro di questo immobile", "error.user_already_member": "L'utente è già membro di questo immobile",
"error.properties_limit_reached": "Hai raggiunto il numero massimo di immobili per il tuo abbonamento", "error.properties_limit_reached": "Hai raggiunto il numero massimo di immobili per il tuo abbonamento",
"error.task_already_cancelled": "L'attività è già stata annullata", "error.task_already_cancelled": "L'attività è già stata annullata",
"error.task_already_archived": "L'attività è già stata archiviata", "error.task_already_archived": "L'attività è già stata archiviata",
"error.failed_to_parse_form": "Impossibile analizzare il modulo multipart", "error.failed_to_parse_form": "Impossibile analizzare il modulo multipart",
"error.task_id_required": "task_id è obbligatorio", "error.task_id_required": "task_id è obbligatorio",
"error.invalid_task_id_value": "task_id non valido", "error.invalid_task_id_value": "task_id non valido",
@@ -64,14 +59,12 @@
"error.invalid_residence_id_value": "residence_id non valido", "error.invalid_residence_id_value": "residence_id non valido",
"error.title_required": "title è obbligatorio", "error.title_required": "title è obbligatorio",
"error.failed_to_upload_file": "Impossibile caricare il file", "error.failed_to_upload_file": "Impossibile caricare il file",
"message.logged_out": "Disconnessione avvenuta con successo", "message.logged_out": "Disconnessione avvenuta con successo",
"message.email_verified": "Email verificata con successo", "message.email_verified": "Email verificata con successo",
"message.verification_email_sent": "Email di verifica inviata", "message.verification_email_sent": "Email di verifica inviata",
"message.password_reset_email_sent": "Se esiste un account con quell'email, è stato inviato un codice di reimpostazione password.", "message.password_reset_email_sent": "Se esiste un account con quell'email, è stato inviato un codice di reimpostazione password.",
"message.reset_code_verified": "Codice verificato con successo", "message.reset_code_verified": "Codice verificato con successo",
"message.password_reset_success": "Password reimpostata con successo. Accedi con la tua nuova password.", "message.password_reset_success": "Password reimpostata con successo. Accedi con la tua nuova password.",
"message.task_deleted": "Attività eliminata con successo", "message.task_deleted": "Attività eliminata con successo",
"message.task_in_progress": "Attività contrassegnata come in corso", "message.task_in_progress": "Attività contrassegnata come in corso",
"message.task_cancelled": "Attività annullata", "message.task_cancelled": "Attività annullata",
@@ -79,46 +72,35 @@
"message.task_archived": "Attività archiviata", "message.task_archived": "Attività archiviata",
"message.task_unarchived": "Attività ripristinata dall'archivio", "message.task_unarchived": "Attività ripristinata dall'archivio",
"message.completion_deleted": "Completamento eliminato con successo", "message.completion_deleted": "Completamento eliminato con successo",
"message.residence_deleted": "Immobile eliminato con successo", "message.residence_deleted": "Immobile eliminato con successo",
"message.user_removed": "Utente rimosso dall'immobile", "message.user_removed": "Utente rimosso dall'immobile",
"message.tasks_report_generated": "Report attività generato con successo", "message.tasks_report_generated": "Report attività generato con successo",
"message.tasks_report_sent": "Report attività generato e inviato a {{.Email}}", "message.tasks_report_sent": "Report attività generato e inviato a {{.Email}}",
"message.tasks_report_email_failed": "Report attività generato ma l'email non è stata inviata", "message.tasks_report_email_failed": "Report attività generato ma l'email non è stata inviata",
"message.contractor_deleted": "Fornitore eliminato con successo", "message.contractor_deleted": "Fornitore eliminato con successo",
"message.document_deleted": "Documento eliminato con successo", "message.document_deleted": "Documento eliminato con successo",
"message.document_activated": "Documento attivato", "message.document_activated": "Documento attivato",
"message.document_deactivated": "Documento disattivato", "message.document_deactivated": "Documento disattivato",
"message.notification_marked_read": "Notifica contrassegnata come letta", "message.notification_marked_read": "Notifica contrassegnata come letta",
"message.all_notifications_marked_read": "Tutte le notifiche contrassegnate come lette", "message.all_notifications_marked_read": "Tutte le notifiche contrassegnate come lette",
"message.device_removed": "Dispositivo rimosso", "message.device_removed": "Dispositivo rimosso",
"message.subscription_upgraded": "Abbonamento aggiornato con successo", "message.subscription_upgraded": "Abbonamento aggiornato con successo",
"message.subscription_cancelled": "Abbonamento annullato. Manterrai i vantaggi Pro fino alla fine del periodo di fatturazione.", "message.subscription_cancelled": "Abbonamento annullato. Manterrai i vantaggi Pro fino alla fine del periodo di fatturazione.",
"message.subscription_restored": "Abbonamento ripristinato con successo", "message.subscription_restored": "Abbonamento ripristinato con successo",
"message.file_deleted": "File eliminato con successo", "message.file_deleted": "File eliminato con successo",
"message.static_data_refreshed": "Dati statici aggiornati", "message.static_data_refreshed": "Dati statici aggiornati",
"error.notification_not_found": "Notifica non trovata", "error.notification_not_found": "Notifica non trovata",
"error.invalid_platform": "Piattaforma non valida", "error.invalid_platform": "Piattaforma non valida",
"error.upgrade_trigger_not_found": "Trigger di aggiornamento non trovato", "error.upgrade_trigger_not_found": "Trigger di aggiornamento non trovato",
"error.receipt_data_required": "receipt_data è obbligatorio per iOS", "error.receipt_data_required": "receipt_data è obbligatorio per iOS",
"error.purchase_token_required": "purchase_token è obbligatorio per Android", "error.purchase_token_required": "purchase_token è obbligatorio per Android",
"error.no_file_provided": "Nessun file fornito", "error.no_file_provided": "Nessun file fornito",
"error.failed_to_fetch_residence_types": "Impossibile recuperare i tipi di immobile", "error.failed_to_fetch_residence_types": "Impossibile recuperare i tipi di immobile",
"error.failed_to_fetch_task_categories": "Impossibile recuperare le categorie di attività", "error.failed_to_fetch_task_categories": "Impossibile recuperare le categorie di attività",
"error.failed_to_fetch_task_priorities": "Impossibile recuperare le priorità delle attività", "error.failed_to_fetch_task_priorities": "Impossibile recuperare le priorità delle attività",
"error.failed_to_fetch_task_frequencies": "Impossibile recuperare le frequenze delle attività", "error.failed_to_fetch_task_frequencies": "Impossibile recuperare le frequenze delle attività",
"error.failed_to_fetch_task_statuses": "Impossibile recuperare gli stati delle attività", "error.failed_to_fetch_task_statuses": "Impossibile recuperare gli stati delle attività",
"error.failed_to_fetch_contractor_specialties": "Impossibile recuperare le specializzazioni dei fornitori", "error.failed_to_fetch_contractor_specialties": "Impossibile recuperare le specializzazioni dei fornitori",
"push.task_due_soon.title": "Attività in Scadenza", "push.task_due_soon.title": "Attività in Scadenza",
"push.task_due_soon.body": "{{.TaskTitle}} scade {{.DueDate}}", "push.task_due_soon.body": "{{.TaskTitle}} scade {{.DueDate}}",
"push.task_overdue.title": "Attività Scaduta", "push.task_overdue.title": "Attività Scaduta",
@@ -129,22 +111,19 @@
"push.task_assigned.body": "Ti è stata assegnata {{.TaskTitle}}", "push.task_assigned.body": "Ti è stata assegnata {{.TaskTitle}}",
"push.residence_shared.title": "Immobile Condiviso", "push.residence_shared.title": "Immobile Condiviso",
"push.residence_shared.body": "{{.UserName}} ha condiviso {{.ResidenceName}} con te", "push.residence_shared.body": "{{.UserName}} ha condiviso {{.ResidenceName}} con te",
"email.welcome.subject": "Benvenuto su honeyDue!", "email.welcome.subject": "Benvenuto su honeyDue!",
"email.verification.subject": "Verifica la Tua Email", "email.verification.subject": "Verifica la Tua Email",
"email.password_reset.subject": "Codice di Reimpostazione Password", "email.password_reset.subject": "Codice di Reimpostazione Password",
"email.tasks_report.subject": "Report Attività per {{.ResidenceName}}", "email.tasks_report.subject": "Report Attività per {{.ResidenceName}}",
"lookup.residence_type.house": "Casa", "lookup.residence_type.house": "Casa",
"lookup.residence_type.apartment": "Appartamento", "lookup.residence_type.apartment": "Appartamento",
"lookup.residence_type.condo": "Condominio", "lookup.residence_type.condo": "Condominio",
"lookup.residence_type.townhouse": "Villetta a Schiera", "lookup.residence_type.townhouse": "Villetta a schiera",
"lookup.residence_type.mobile_home": "Casa Mobile", "lookup.residence_type.mobile_home": "Casa mobile",
"lookup.residence_type.other": "Altro", "lookup.residence_type.other": "Altro",
"lookup.task_category.plumbing": "Idraulica", "lookup.task_category.plumbing": "Idraulica",
"lookup.task_category.electrical": "Elettricità", "lookup.task_category.electrical": "Elettrico",
"lookup.task_category.hvac": "Climatizzazione", "lookup.task_category.hvac": "HVAC",
"lookup.task_category.appliances": "Elettrodomestici", "lookup.task_category.appliances": "Elettrodomestici",
"lookup.task_category.exterior": "Esterno", "lookup.task_category.exterior": "Esterno",
"lookup.task_category.interior": "Interno", "lookup.task_category.interior": "Interno",
@@ -154,38 +133,115 @@
"lookup.task_category.pest_control": "Disinfestazione", "lookup.task_category.pest_control": "Disinfestazione",
"lookup.task_category.seasonal": "Stagionale", "lookup.task_category.seasonal": "Stagionale",
"lookup.task_category.other": "Altro", "lookup.task_category.other": "Altro",
"lookup.task_priority.low": "Bassa", "lookup.task_priority.low": "Bassa",
"lookup.task_priority.medium": "Media", "lookup.task_priority.medium": "Media",
"lookup.task_priority.high": "Alta", "lookup.task_priority.high": "Alta",
"lookup.task_priority.urgent": "Urgente", "lookup.task_priority.urgent": "Urgente",
"lookup.task_status.pending": "In Attesa", "lookup.task_status.pending": "In Attesa",
"lookup.task_status.in_progress": "In Corso", "lookup.task_status.in_progress": "In Corso",
"lookup.task_status.completed": "Completata", "lookup.task_status.completed": "Completata",
"lookup.task_status.cancelled": "Annullata", "lookup.task_status.cancelled": "Annullata",
"lookup.task_status.archived": "Archiviata", "lookup.task_status.archived": "Archiviata",
"lookup.task_frequency.once": "Una volta",
"lookup.task_frequency.once": "Una Volta", "lookup.task_frequency.daily": "Giornaliero",
"lookup.task_frequency.daily": "Giornaliera",
"lookup.task_frequency.weekly": "Settimanale", "lookup.task_frequency.weekly": "Settimanale",
"lookup.task_frequency.biweekly": "Ogni 2 Settimane", "lookup.task_frequency.biweekly": "Ogni 2 Settimane",
"lookup.task_frequency.monthly": "Mensile", "lookup.task_frequency.monthly": "Mensile",
"lookup.task_frequency.quarterly": "Trimestrale", "lookup.task_frequency.quarterly": "Trimestrale",
"lookup.task_frequency.semiannually": "Ogni 6 Mesi", "lookup.task_frequency.semiannually": "Ogni 6 Mesi",
"lookup.task_frequency.annually": "Annuale", "lookup.task_frequency.annually": "Annuale",
"lookup.contractor_specialty.plumber": "Idraulico", "lookup.contractor_specialty.plumber": "Idraulico",
"lookup.contractor_specialty.electrician": "Elettricista", "lookup.contractor_specialty.electrician": "Elettricista",
"lookup.contractor_specialty.hvac_technician": "Tecnico Climatizzazione", "lookup.contractor_specialty.hvac_technician": "Tecnico HVAC",
"lookup.contractor_specialty.handyman": "Tuttofare", "lookup.contractor_specialty.handyman": "Tuttofare",
"lookup.contractor_specialty.landscaper": "Giardiniere", "lookup.contractor_specialty.landscaper": "Giardiniere",
"lookup.contractor_specialty.roofer": "Lattoniere", "lookup.contractor_specialty.roofer": "Conciatetti",
"lookup.contractor_specialty.painter": "Imbianchino", "lookup.contractor_specialty.painter": "Imbianchino",
"lookup.contractor_specialty.carpenter": "Falegname", "lookup.contractor_specialty.carpenter": "Falegname",
"lookup.contractor_specialty.pest_control": "Disinfestazione", "lookup.contractor_specialty.pest_control": "Disinfestazione",
"lookup.contractor_specialty.cleaning": "Pulizia", "lookup.contractor_specialty.cleaning": "Pulizia",
"lookup.contractor_specialty.pool_service": "Manutenzione Piscine", "lookup.contractor_specialty.pool_service": "Servizio piscina",
"lookup.contractor_specialty.general_contractor": "Impresa Generale", "lookup.contractor_specialty.general_contractor": "Imprenditore generale",
"lookup.contractor_specialty.other": "Altro" "lookup.contractor_specialty.other": "Altro",
"suggestion.reason.has_pool": "La tua casa ha una piscina",
"suggestion.reason.has_sprinkler_system": "La tua casa ha un impianto di irrigazione",
"suggestion.reason.has_septic": "La tua casa ha una fossa settica",
"suggestion.reason.has_fireplace": "La tua casa ha un camino",
"suggestion.reason.has_garage": "La tua casa ha un garage",
"suggestion.reason.has_basement": "La tua casa ha un seminterrato",
"suggestion.reason.has_attic": "La tua casa ha una soffitta",
"suggestion.reason.heating_type": "Corrisponde al tuo impianto di riscaldamento",
"suggestion.reason.cooling_type": "Corrisponde al tuo impianto di raffreddamento",
"suggestion.reason.water_heater_type": "Corrisponde al tuo scaldabagno",
"suggestion.reason.roof_type": "Corrisponde al tuo tetto",
"suggestion.reason.exterior_type": "Corrisponde al tuo esterno",
"suggestion.reason.flooring_primary": "Corrisponde alla tua pavimentazione",
"suggestion.reason.landscaping_type": "Corrisponde al tuo giardino",
"suggestion.reason.property_type": "Consigliato per il tuo tipo di immobile",
"suggestion.reason.climate_region": "Consigliato per il tuo clima",
"lookup.residence_type.duplex": "Bifamiliare",
"lookup.residence_type.vacation_home": "Casa vacanze",
"lookup.task_category.general": "Generale",
"lookup.task_frequency.bi_weekly": "Bisettimanale",
"lookup.task_frequency.semi_annually": "Semestrale",
"lookup.task_frequency.custom": "Personalizzato",
"lookup.contractor_specialty.appliance_repair": "Riparazione elettrodomestici",
"lookup.contractor_specialty.cleaner": "Addetto alle pulizie",
"lookup.contractor_specialty.locksmith": "Fabbro",
"lookup.home_profile.gas_furnace": "Caldaia a gas",
"lookup.home_profile.electric_furnace": "Caldaia elettrica",
"lookup.home_profile.heat_pump": "Pompa di calore",
"lookup.home_profile.boiler": "Caldaia",
"lookup.home_profile.radiant": "Radiante",
"lookup.home_profile.other": "Altro",
"lookup.home_profile.central_ac": "Climatizzatore centrale",
"lookup.home_profile.window_ac": "Climatizzatore a finestra",
"lookup.home_profile.evaporative": "Evaporativo",
"lookup.home_profile.none": "Nessuno",
"lookup.home_profile.tank_gas": "Serbatoio (gas)",
"lookup.home_profile.tank_electric": "Serbatoio (elettrico)",
"lookup.home_profile.tankless_gas": "Senza serbatoio (gas)",
"lookup.home_profile.tankless_electric": "Senza serbatoio (elettrico)",
"lookup.home_profile.solar": "Solare",
"lookup.home_profile.asphalt_shingle": "Tegola bituminosa",
"lookup.home_profile.metal": "Metallo",
"lookup.home_profile.tile": "Tegola",
"lookup.home_profile.slate": "Ardesia",
"lookup.home_profile.wood_shake": "Scandola di legno",
"lookup.home_profile.flat": "Piatto",
"lookup.home_profile.brick": "Mattone",
"lookup.home_profile.vinyl_siding": "Rivestimento in vinile",
"lookup.home_profile.wood_siding": "Rivestimento in legno",
"lookup.home_profile.stucco": "Stucco",
"lookup.home_profile.stone": "Pietra",
"lookup.home_profile.fiber_cement": "Fibrocemento",
"lookup.home_profile.hardwood": "Legno duro",
"lookup.home_profile.laminate": "Laminato",
"lookup.home_profile.carpet": "Moquette",
"lookup.home_profile.vinyl": "Vinile",
"lookup.home_profile.concrete": "Cemento",
"lookup.home_profile.lawn": "Prato",
"lookup.home_profile.desert": "Deserto",
"lookup.home_profile.xeriscape": "Xeriscaping",
"lookup.home_profile.garden": "Giardino",
"lookup.home_profile.mixed": "Misto",
"lookup.document_type.warranty": "Garanzia",
"lookup.document_type.manual": "Manuale d'uso",
"lookup.document_type.receipt": "Ricevuta/Fattura",
"lookup.document_type.inspection": "Rapporto di ispezione",
"lookup.document_type.permit": "Permesso",
"lookup.document_type.deed": "Atto/Titolo",
"lookup.document_type.insurance": "Assicurazione",
"lookup.document_type.contract": "Contratto",
"lookup.document_type.photo": "Foto",
"lookup.document_type.other": "Altro",
"lookup.document_category.appliance": "Elettrodomestico",
"lookup.document_category.hvac": "HVAC",
"lookup.document_category.plumbing": "Idraulica",
"lookup.document_category.electrical": "Elettrico",
"lookup.document_category.roofing": "Tetto",
"lookup.document_category.structural": "Strutturale",
"lookup.document_category.landscaping": "Giardinaggio",
"lookup.document_category.general": "Generale",
"lookup.document_category.other": "Altro"
} }
+90 -34
View File
@@ -25,7 +25,6 @@
"error.google_signin_not_configured": "Google サインインが設定されていません", "error.google_signin_not_configured": "Google サインインが設定されていません",
"error.google_signin_failed": "Google サインインに失敗しました", "error.google_signin_failed": "Google サインインに失敗しました",
"error.invalid_google_token": "無効な Google ID トークンです", "error.invalid_google_token": "無効な Google ID トークンです",
"error.invalid_task_id": "無効なタスクIDです", "error.invalid_task_id": "無効なタスクIDです",
"error.invalid_residence_id": "無効な物件IDです", "error.invalid_residence_id": "無効な物件IDです",
"error.invalid_contractor_id": "無効な業者IDです", "error.invalid_contractor_id": "無効な業者IDです",
@@ -34,7 +33,6 @@
"error.invalid_user_id": "無効なユーザーIDです", "error.invalid_user_id": "無効なユーザーIDです",
"error.invalid_notification_id": "無効な通知IDです", "error.invalid_notification_id": "無効な通知IDです",
"error.invalid_device_id": "無効なデバイスIDです", "error.invalid_device_id": "無効なデバイスIDです",
"error.task_not_found": "タスクが見つかりません", "error.task_not_found": "タスクが見つかりません",
"error.residence_not_found": "物件が見つかりません", "error.residence_not_found": "物件が見つかりません",
"error.contractor_not_found": "業者が見つかりません", "error.contractor_not_found": "業者が見つかりません",
@@ -43,7 +41,6 @@
"error.user_not_found": "ユーザーが見つかりません", "error.user_not_found": "ユーザーが見つかりません",
"error.share_code_invalid": "無効な共有コードです", "error.share_code_invalid": "無効な共有コードです",
"error.share_code_expired": "共有コードの有効期限が切れています", "error.share_code_expired": "共有コードの有効期限が切れています",
"error.task_access_denied": "このタスクへのアクセス権がありません", "error.task_access_denied": "このタスクへのアクセス権がありません",
"error.residence_access_denied": "この物件へのアクセス権がありません", "error.residence_access_denied": "この物件へのアクセス権がありません",
"error.contractor_access_denied": "この業者へのアクセス権がありません", "error.contractor_access_denied": "この業者へのアクセス権がありません",
@@ -52,10 +49,8 @@
"error.cannot_remove_owner": "物件のオーナーを削除することはできません", "error.cannot_remove_owner": "物件のオーナーを削除することはできません",
"error.user_already_member": "このユーザーは既にこの物件のメンバーです", "error.user_already_member": "このユーザーは既にこの物件のメンバーです",
"error.properties_limit_reached": "サブスクリプションで許可されている物件の最大数に達しました", "error.properties_limit_reached": "サブスクリプションで許可されている物件の最大数に達しました",
"error.task_already_cancelled": "タスクは既にキャンセルされています", "error.task_already_cancelled": "タスクは既にキャンセルされています",
"error.task_already_archived": "タスクは既にアーカイブされています", "error.task_already_archived": "タスクは既にアーカイブされています",
"error.failed_to_parse_form": "マルチパートフォームの解析に失敗しました", "error.failed_to_parse_form": "マルチパートフォームの解析に失敗しました",
"error.task_id_required": "task_id は必須です", "error.task_id_required": "task_id は必須です",
"error.invalid_task_id_value": "無効な task_id です", "error.invalid_task_id_value": "無効な task_id です",
@@ -64,14 +59,12 @@
"error.invalid_residence_id_value": "無効な residence_id です", "error.invalid_residence_id_value": "無効な residence_id です",
"error.title_required": "タイトルは必須です", "error.title_required": "タイトルは必須です",
"error.failed_to_upload_file": "ファイルのアップロードに失敗しました", "error.failed_to_upload_file": "ファイルのアップロードに失敗しました",
"message.logged_out": "ログアウトしました", "message.logged_out": "ログアウトしました",
"message.email_verified": "メールアドレスの認証が完了しました", "message.email_verified": "メールアドレスの認証が完了しました",
"message.verification_email_sent": "認証メールを送信しました", "message.verification_email_sent": "認証メールを送信しました",
"message.password_reset_email_sent": "該当するアカウントが存在する場合、パスワードリセットコードが送信されました。", "message.password_reset_email_sent": "該当するアカウントが存在する場合、パスワードリセットコードが送信されました。",
"message.reset_code_verified": "コードの認証が完了しました", "message.reset_code_verified": "コードの認証が完了しました",
"message.password_reset_success": "パスワードのリセットが完了しました。新しいパスワードでログインしてください。", "message.password_reset_success": "パスワードのリセットが完了しました。新しいパスワードでログインしてください。",
"message.task_deleted": "タスクを削除しました", "message.task_deleted": "タスクを削除しました",
"message.task_in_progress": "タスクを進行中に設定しました", "message.task_in_progress": "タスクを進行中に設定しました",
"message.task_cancelled": "タスクをキャンセルしました", "message.task_cancelled": "タスクをキャンセルしました",
@@ -79,46 +72,35 @@
"message.task_archived": "タスクをアーカイブしました", "message.task_archived": "タスクをアーカイブしました",
"message.task_unarchived": "タスクのアーカイブを解除しました", "message.task_unarchived": "タスクのアーカイブを解除しました",
"message.completion_deleted": "完了記録を削除しました", "message.completion_deleted": "完了記録を削除しました",
"message.residence_deleted": "物件を削除しました", "message.residence_deleted": "物件を削除しました",
"message.user_removed": "ユーザーを物件から削除しました", "message.user_removed": "ユーザーを物件から削除しました",
"message.tasks_report_generated": "タスクレポートを生成しました", "message.tasks_report_generated": "タスクレポートを生成しました",
"message.tasks_report_sent": "タスクレポートを生成し、{{.Email}} に送信しました", "message.tasks_report_sent": "タスクレポートを生成し、{{.Email}} に送信しました",
"message.tasks_report_email_failed": "タスクレポートは生成されましたが、メールの送信に失敗しました", "message.tasks_report_email_failed": "タスクレポートは生成されましたが、メールの送信に失敗しました",
"message.contractor_deleted": "業者を削除しました", "message.contractor_deleted": "業者を削除しました",
"message.document_deleted": "書類を削除しました", "message.document_deleted": "書類を削除しました",
"message.document_activated": "書類を有効化しました", "message.document_activated": "書類を有効化しました",
"message.document_deactivated": "書類を無効化しました", "message.document_deactivated": "書類を無効化しました",
"message.notification_marked_read": "通知を既読にしました", "message.notification_marked_read": "通知を既読にしました",
"message.all_notifications_marked_read": "すべての通知を既読にしました", "message.all_notifications_marked_read": "すべての通知を既読にしました",
"message.device_removed": "デバイスを削除しました", "message.device_removed": "デバイスを削除しました",
"message.subscription_upgraded": "サブスクリプションをアップグレードしました", "message.subscription_upgraded": "サブスクリプションをアップグレードしました",
"message.subscription_cancelled": "サブスクリプションをキャンセルしました。請求期間終了まで Pro 機能をご利用いただけます。", "message.subscription_cancelled": "サブスクリプションをキャンセルしました。請求期間終了まで Pro 機能をご利用いただけます。",
"message.subscription_restored": "サブスクリプションを復元しました", "message.subscription_restored": "サブスクリプションを復元しました",
"message.file_deleted": "ファイルを削除しました", "message.file_deleted": "ファイルを削除しました",
"message.static_data_refreshed": "静的データを更新しました", "message.static_data_refreshed": "静的データを更新しました",
"error.notification_not_found": "通知が見つかりません", "error.notification_not_found": "通知が見つかりません",
"error.invalid_platform": "無効なプラットフォームです", "error.invalid_platform": "無効なプラットフォームです",
"error.upgrade_trigger_not_found": "アップグレードトリガーが見つかりません", "error.upgrade_trigger_not_found": "アップグレードトリガーが見つかりません",
"error.receipt_data_required": "iOS の場合、receipt_data は必須です", "error.receipt_data_required": "iOS の場合、receipt_data は必須です",
"error.purchase_token_required": "Android の場合、purchase_token は必須です", "error.purchase_token_required": "Android の場合、purchase_token は必須です",
"error.no_file_provided": "ファイルが提供されていません", "error.no_file_provided": "ファイルが提供されていません",
"error.failed_to_fetch_residence_types": "物件タイプの取得に失敗しました", "error.failed_to_fetch_residence_types": "物件タイプの取得に失敗しました",
"error.failed_to_fetch_task_categories": "タスクカテゴリの取得に失敗しました", "error.failed_to_fetch_task_categories": "タスクカテゴリの取得に失敗しました",
"error.failed_to_fetch_task_priorities": "タスク優先度の取得に失敗しました", "error.failed_to_fetch_task_priorities": "タスク優先度の取得に失敗しました",
"error.failed_to_fetch_task_frequencies": "タスク頻度の取得に失敗しました", "error.failed_to_fetch_task_frequencies": "タスク頻度の取得に失敗しました",
"error.failed_to_fetch_task_statuses": "タスクステータスの取得に失敗しました", "error.failed_to_fetch_task_statuses": "タスクステータスの取得に失敗しました",
"error.failed_to_fetch_contractor_specialties": "業者専門分野の取得に失敗しました", "error.failed_to_fetch_contractor_specialties": "業者専門分野の取得に失敗しました",
"push.task_due_soon.title": "タスクの期限が近づいています", "push.task_due_soon.title": "タスクの期限が近づいています",
"push.task_due_soon.body": "{{.TaskTitle}} の期限は {{.DueDate}} です", "push.task_due_soon.body": "{{.TaskTitle}} の期限は {{.DueDate}} です",
"push.task_overdue.title": "期限切れのタスク", "push.task_overdue.title": "期限切れのタスク",
@@ -129,19 +111,16 @@
"push.task_assigned.body": "{{.TaskTitle}} に割り当てられました", "push.task_assigned.body": "{{.TaskTitle}} に割り当てられました",
"push.residence_shared.title": "物件が共有されました", "push.residence_shared.title": "物件が共有されました",
"push.residence_shared.body": "{{.UserName}} が {{.ResidenceName}} を共有しました", "push.residence_shared.body": "{{.UserName}} が {{.ResidenceName}} を共有しました",
"email.welcome.subject": "honeyDue へようこそ!", "email.welcome.subject": "honeyDue へようこそ!",
"email.verification.subject": "メールアドレスの認証", "email.verification.subject": "メールアドレスの認証",
"email.password_reset.subject": "パスワードリセットコード", "email.password_reset.subject": "パスワードリセットコード",
"email.tasks_report.subject": "{{.ResidenceName}} のタスクレポート", "email.tasks_report.subject": "{{.ResidenceName}} のタスクレポート",
"lookup.residence_type.house": "一戸建て", "lookup.residence_type.house": "一戸建て",
"lookup.residence_type.apartment": "アパート", "lookup.residence_type.apartment": "アパート",
"lookup.residence_type.condo": "マンション", "lookup.residence_type.condo": "分譲マンション",
"lookup.residence_type.townhouse": "タウンハウス", "lookup.residence_type.townhouse": "タウンハウス",
"lookup.residence_type.mobile_home": "移動式住宅", "lookup.residence_type.mobile_home": "モバイルホーム",
"lookup.residence_type.other": "その他", "lookup.residence_type.other": "その他",
"lookup.task_category.plumbing": "配管", "lookup.task_category.plumbing": "配管",
"lookup.task_category.electrical": "電気", "lookup.task_category.electrical": "電気",
"lookup.task_category.hvac": "空調", "lookup.task_category.hvac": "空調",
@@ -154,19 +133,16 @@
"lookup.task_category.pest_control": "害虫駆除", "lookup.task_category.pest_control": "害虫駆除",
"lookup.task_category.seasonal": "季節", "lookup.task_category.seasonal": "季節",
"lookup.task_category.other": "その他", "lookup.task_category.other": "その他",
"lookup.task_priority.low": "低", "lookup.task_priority.low": "低",
"lookup.task_priority.medium": "中", "lookup.task_priority.medium": "中",
"lookup.task_priority.high": "高", "lookup.task_priority.high": "高",
"lookup.task_priority.urgent": "緊急", "lookup.task_priority.urgent": "緊急",
"lookup.task_status.pending": "保留中", "lookup.task_status.pending": "保留中",
"lookup.task_status.in_progress": "進行中", "lookup.task_status.in_progress": "進行中",
"lookup.task_status.completed": "完了", "lookup.task_status.completed": "完了",
"lookup.task_status.cancelled": "キャンセル", "lookup.task_status.cancelled": "キャンセル",
"lookup.task_status.archived": "アーカイブ", "lookup.task_status.archived": "アーカイブ",
"lookup.task_frequency.once": "1回",
"lookup.task_frequency.once": "一度のみ",
"lookup.task_frequency.daily": "毎日", "lookup.task_frequency.daily": "毎日",
"lookup.task_frequency.weekly": "毎週", "lookup.task_frequency.weekly": "毎週",
"lookup.task_frequency.biweekly": "2週間ごと", "lookup.task_frequency.biweekly": "2週間ごと",
@@ -174,18 +150,98 @@
"lookup.task_frequency.quarterly": "四半期ごと", "lookup.task_frequency.quarterly": "四半期ごと",
"lookup.task_frequency.semiannually": "半年ごと", "lookup.task_frequency.semiannually": "半年ごと",
"lookup.task_frequency.annually": "毎年", "lookup.task_frequency.annually": "毎年",
"lookup.contractor_specialty.plumber": "配管工", "lookup.contractor_specialty.plumber": "配管工",
"lookup.contractor_specialty.electrician": "電気工事士", "lookup.contractor_specialty.electrician": "電気技師",
"lookup.contractor_specialty.hvac_technician": "空調技術者", "lookup.contractor_specialty.hvac_technician": "空調技",
"lookup.contractor_specialty.handyman": "便利屋", "lookup.contractor_specialty.handyman": "便利屋",
"lookup.contractor_specialty.landscaper": "造園業者", "lookup.contractor_specialty.landscaper": "造園業者",
"lookup.contractor_specialty.roofer": "屋根職人", "lookup.contractor_specialty.roofer": "屋根職人",
"lookup.contractor_specialty.painter": "塗装", "lookup.contractor_specialty.painter": "塗装業者",
"lookup.contractor_specialty.carpenter": "大工", "lookup.contractor_specialty.carpenter": "大工",
"lookup.contractor_specialty.pest_control": "害虫駆除業者", "lookup.contractor_specialty.pest_control": "害虫駆除",
"lookup.contractor_specialty.cleaning": "清掃業者", "lookup.contractor_specialty.cleaning": "清掃業者",
"lookup.contractor_specialty.pool_service": "プールサービス", "lookup.contractor_specialty.pool_service": "プールサービス",
"lookup.contractor_specialty.general_contractor": "総合建設業者", "lookup.contractor_specialty.general_contractor": "総合請負業者",
"lookup.contractor_specialty.other": "その他" "lookup.contractor_specialty.other": "その他",
"suggestion.reason.has_pool": "ご自宅にプールがあります",
"suggestion.reason.has_sprinkler_system": "ご自宅にスプリンクラーがあります",
"suggestion.reason.has_septic": "ご自宅に浄化槽があります",
"suggestion.reason.has_fireplace": "ご自宅に暖炉があります",
"suggestion.reason.has_garage": "ご自宅にガレージがあります",
"suggestion.reason.has_basement": "ご自宅に地下室があります",
"suggestion.reason.has_attic": "ご自宅に屋根裏があります",
"suggestion.reason.heating_type": "暖房設備に合っています",
"suggestion.reason.cooling_type": "冷房設備に合っています",
"suggestion.reason.water_heater_type": "給湯器に合っています",
"suggestion.reason.roof_type": "屋根に合っています",
"suggestion.reason.exterior_type": "外装に合っています",
"suggestion.reason.flooring_primary": "床材に合っています",
"suggestion.reason.landscaping_type": "造園に合っています",
"suggestion.reason.property_type": "ご自宅の種類におすすめです",
"suggestion.reason.climate_region": "お住まいの気候におすすめです",
"lookup.residence_type.duplex": "二世帯住宅",
"lookup.residence_type.vacation_home": "別荘",
"lookup.task_category.general": "一般",
"lookup.task_frequency.bi_weekly": "隔週",
"lookup.task_frequency.semi_annually": "半年ごと",
"lookup.task_frequency.custom": "カスタム",
"lookup.contractor_specialty.appliance_repair": "家電修理",
"lookup.contractor_specialty.cleaner": "清掃業者",
"lookup.contractor_specialty.locksmith": "錠前師",
"lookup.home_profile.gas_furnace": "ガス炉",
"lookup.home_profile.electric_furnace": "電気炉",
"lookup.home_profile.heat_pump": "ヒートポンプ",
"lookup.home_profile.boiler": "ボイラー",
"lookup.home_profile.radiant": "放射式",
"lookup.home_profile.other": "その他",
"lookup.home_profile.central_ac": "セントラルエアコン",
"lookup.home_profile.window_ac": "窓用エアコン",
"lookup.home_profile.evaporative": "気化式",
"lookup.home_profile.none": "なし",
"lookup.home_profile.tank_gas": "タンク式(ガス)",
"lookup.home_profile.tank_electric": "タンク式(電気)",
"lookup.home_profile.tankless_gas": "タンクレス(ガス)",
"lookup.home_profile.tankless_electric": "タンクレス(電気)",
"lookup.home_profile.solar": "ソーラー",
"lookup.home_profile.asphalt_shingle": "アスファルトシングル",
"lookup.home_profile.metal": "金属",
"lookup.home_profile.tile": "タイル",
"lookup.home_profile.slate": "スレート",
"lookup.home_profile.wood_shake": "木製シェイク",
"lookup.home_profile.flat": "平型",
"lookup.home_profile.brick": "レンガ",
"lookup.home_profile.vinyl_siding": "ビニールサイディング",
"lookup.home_profile.wood_siding": "木製サイディング",
"lookup.home_profile.stucco": "スタッコ",
"lookup.home_profile.stone": "石",
"lookup.home_profile.fiber_cement": "繊維強化セメント",
"lookup.home_profile.hardwood": "無垢材",
"lookup.home_profile.laminate": "ラミネート",
"lookup.home_profile.carpet": "カーペット",
"lookup.home_profile.vinyl": "ビニール",
"lookup.home_profile.concrete": "コンクリート",
"lookup.home_profile.lawn": "芝生",
"lookup.home_profile.desert": "砂漠",
"lookup.home_profile.xeriscape": "ゼリスケープ",
"lookup.home_profile.garden": "庭園",
"lookup.home_profile.mixed": "混合",
"lookup.document_type.warranty": "保証",
"lookup.document_type.manual": "ユーザーマニュアル",
"lookup.document_type.receipt": "領収書/請求書",
"lookup.document_type.inspection": "点検報告書",
"lookup.document_type.permit": "許可証",
"lookup.document_type.deed": "権利証/証書",
"lookup.document_type.insurance": "保険",
"lookup.document_type.contract": "契約",
"lookup.document_type.photo": "写真",
"lookup.document_type.other": "その他",
"lookup.document_category.appliance": "家電",
"lookup.document_category.hvac": "空調",
"lookup.document_category.plumbing": "配管",
"lookup.document_category.electrical": "電気",
"lookup.document_category.roofing": "屋根",
"lookup.document_category.structural": "構造",
"lookup.document_category.landscaping": "造園",
"lookup.document_category.general": "一般",
"lookup.document_category.other": "その他"
} }
+89 -33
View File
@@ -25,7 +25,6 @@
"error.google_signin_not_configured": "Google 로그인이 설정되지 않았습니다", "error.google_signin_not_configured": "Google 로그인이 설정되지 않았습니다",
"error.google_signin_failed": "Google 로그인에 실패했습니다", "error.google_signin_failed": "Google 로그인에 실패했습니다",
"error.invalid_google_token": "유효하지 않은 Google 인증 토큰입니다", "error.invalid_google_token": "유효하지 않은 Google 인증 토큰입니다",
"error.invalid_task_id": "유효하지 않은 작업 ID입니다", "error.invalid_task_id": "유효하지 않은 작업 ID입니다",
"error.invalid_residence_id": "유효하지 않은 주거지 ID입니다", "error.invalid_residence_id": "유효하지 않은 주거지 ID입니다",
"error.invalid_contractor_id": "유효하지 않은 계약업체 ID입니다", "error.invalid_contractor_id": "유효하지 않은 계약업체 ID입니다",
@@ -34,7 +33,6 @@
"error.invalid_user_id": "유효하지 않은 사용자 ID입니다", "error.invalid_user_id": "유효하지 않은 사용자 ID입니다",
"error.invalid_notification_id": "유효하지 않은 알림 ID입니다", "error.invalid_notification_id": "유효하지 않은 알림 ID입니다",
"error.invalid_device_id": "유효하지 않은 기기 ID입니다", "error.invalid_device_id": "유효하지 않은 기기 ID입니다",
"error.task_not_found": "작업을 찾을 수 없습니다", "error.task_not_found": "작업을 찾을 수 없습니다",
"error.residence_not_found": "주거지를 찾을 수 없습니다", "error.residence_not_found": "주거지를 찾을 수 없습니다",
"error.contractor_not_found": "계약업체를 찾을 수 없습니다", "error.contractor_not_found": "계약업체를 찾을 수 없습니다",
@@ -43,7 +41,6 @@
"error.user_not_found": "사용자를 찾을 수 없습니다", "error.user_not_found": "사용자를 찾을 수 없습니다",
"error.share_code_invalid": "유효하지 않은 공유 코드입니다", "error.share_code_invalid": "유효하지 않은 공유 코드입니다",
"error.share_code_expired": "공유 코드가 만료되었습니다", "error.share_code_expired": "공유 코드가 만료되었습니다",
"error.task_access_denied": "이 작업에 접근할 권한이 없습니다", "error.task_access_denied": "이 작업에 접근할 권한이 없습니다",
"error.residence_access_denied": "이 주거지에 접근할 권한이 없습니다", "error.residence_access_denied": "이 주거지에 접근할 권한이 없습니다",
"error.contractor_access_denied": "이 계약업체에 접근할 권한이 없습니다", "error.contractor_access_denied": "이 계약업체에 접근할 권한이 없습니다",
@@ -52,10 +49,8 @@
"error.cannot_remove_owner": "주거지 소유자는 삭제할 수 없습니다", "error.cannot_remove_owner": "주거지 소유자는 삭제할 수 없습니다",
"error.user_already_member": "이미 이 주거지의 멤버입니다", "error.user_already_member": "이미 이 주거지의 멤버입니다",
"error.properties_limit_reached": "구독 플랜의 최대 주거지 수에 도달했습니다", "error.properties_limit_reached": "구독 플랜의 최대 주거지 수에 도달했습니다",
"error.task_already_cancelled": "이미 취소된 작업입니다", "error.task_already_cancelled": "이미 취소된 작업입니다",
"error.task_already_archived": "이미 보관된 작업입니다", "error.task_already_archived": "이미 보관된 작업입니다",
"error.failed_to_parse_form": "멀티파트 폼 파싱에 실패했습니다", "error.failed_to_parse_form": "멀티파트 폼 파싱에 실패했습니다",
"error.task_id_required": "task_id가 필요합니다", "error.task_id_required": "task_id가 필요합니다",
"error.invalid_task_id_value": "유효하지 않은 task_id 값입니다", "error.invalid_task_id_value": "유효하지 않은 task_id 값입니다",
@@ -64,14 +59,12 @@
"error.invalid_residence_id_value": "유효하지 않은 residence_id 값입니다", "error.invalid_residence_id_value": "유효하지 않은 residence_id 값입니다",
"error.title_required": "제목이 필요합니다", "error.title_required": "제목이 필요합니다",
"error.failed_to_upload_file": "파일 업로드에 실패했습니다", "error.failed_to_upload_file": "파일 업로드에 실패했습니다",
"message.logged_out": "로그아웃되었습니다", "message.logged_out": "로그아웃되었습니다",
"message.email_verified": "이메일이 인증되었습니다", "message.email_verified": "이메일이 인증되었습니다",
"message.verification_email_sent": "인증 이메일이 발송되었습니다", "message.verification_email_sent": "인증 이메일이 발송되었습니다",
"message.password_reset_email_sent": "해당 이메일로 등록된 계정이 있는 경우 비밀번호 재설정 코드가 발송되었습니다.", "message.password_reset_email_sent": "해당 이메일로 등록된 계정이 있는 경우 비밀번호 재설정 코드가 발송되었습니다.",
"message.reset_code_verified": "코드가 인증되었습니다", "message.reset_code_verified": "코드가 인증되었습니다",
"message.password_reset_success": "비밀번호가 재설정되었습니다. 새 비밀번호로 로그인해주세요.", "message.password_reset_success": "비밀번호가 재설정되었습니다. 새 비밀번호로 로그인해주세요.",
"message.task_deleted": "작업이 삭제되었습니다", "message.task_deleted": "작업이 삭제되었습니다",
"message.task_in_progress": "작업이 진행 중으로 표시되었습니다", "message.task_in_progress": "작업이 진행 중으로 표시되었습니다",
"message.task_cancelled": "작업이 취소되었습니다", "message.task_cancelled": "작업이 취소되었습니다",
@@ -79,46 +72,35 @@
"message.task_archived": "작업이 보관되었습니다", "message.task_archived": "작업이 보관되었습니다",
"message.task_unarchived": "작업 보관이 해제되었습니다", "message.task_unarchived": "작업 보관이 해제되었습니다",
"message.completion_deleted": "완료 기록이 삭제되었습니다", "message.completion_deleted": "완료 기록이 삭제되었습니다",
"message.residence_deleted": "주거지가 삭제되었습니다", "message.residence_deleted": "주거지가 삭제되었습니다",
"message.user_removed": "주거지에서 사용자가 제거되었습니다", "message.user_removed": "주거지에서 사용자가 제거되었습니다",
"message.tasks_report_generated": "작업 보고서가 생성되었습니다", "message.tasks_report_generated": "작업 보고서가 생성되었습니다",
"message.tasks_report_sent": "작업 보고서가 생성되어 {{.Email}}로 발송되었습니다", "message.tasks_report_sent": "작업 보고서가 생성되어 {{.Email}}로 발송되었습니다",
"message.tasks_report_email_failed": "작업 보고서가 생성되었지만 이메일 발송에 실패했습니다", "message.tasks_report_email_failed": "작업 보고서가 생성되었지만 이메일 발송에 실패했습니다",
"message.contractor_deleted": "계약업체가 삭제되었습니다", "message.contractor_deleted": "계약업체가 삭제되었습니다",
"message.document_deleted": "문서가 삭제되었습니다", "message.document_deleted": "문서가 삭제되었습니다",
"message.document_activated": "문서가 활성화되었습니다", "message.document_activated": "문서가 활성화되었습니다",
"message.document_deactivated": "문서가 비활성화되었습니다", "message.document_deactivated": "문서가 비활성화되었습니다",
"message.notification_marked_read": "알림이 읽음으로 표시되었습니다", "message.notification_marked_read": "알림이 읽음으로 표시되었습니다",
"message.all_notifications_marked_read": "모든 알림이 읽음으로 표시되었습니다", "message.all_notifications_marked_read": "모든 알림이 읽음으로 표시되었습니다",
"message.device_removed": "기기가 제거되었습니다", "message.device_removed": "기기가 제거되었습니다",
"message.subscription_upgraded": "구독이 업그레이드되었습니다", "message.subscription_upgraded": "구독이 업그레이드되었습니다",
"message.subscription_cancelled": "구독이 취소되었습니다. 결제 기간이 종료될 때까지 Pro 혜택을 유지하실 수 있습니다.", "message.subscription_cancelled": "구독이 취소되었습니다. 결제 기간이 종료될 때까지 Pro 혜택을 유지하실 수 있습니다.",
"message.subscription_restored": "구독이 복원되었습니다", "message.subscription_restored": "구독이 복원되었습니다",
"message.file_deleted": "파일이 삭제되었습니다", "message.file_deleted": "파일이 삭제되었습니다",
"message.static_data_refreshed": "정적 데이터가 새로고침되었습니다", "message.static_data_refreshed": "정적 데이터가 새로고침되었습니다",
"error.notification_not_found": "알림을 찾을 수 없습니다", "error.notification_not_found": "알림을 찾을 수 없습니다",
"error.invalid_platform": "유효하지 않은 플랫폼입니다", "error.invalid_platform": "유효하지 않은 플랫폼입니다",
"error.upgrade_trigger_not_found": "업그레이드 트리거를 찾을 수 없습니다", "error.upgrade_trigger_not_found": "업그레이드 트리거를 찾을 수 없습니다",
"error.receipt_data_required": "iOS의 경우 receipt_data가 필요합니다", "error.receipt_data_required": "iOS의 경우 receipt_data가 필요합니다",
"error.purchase_token_required": "Android의 경우 purchase_token이 필요합니다", "error.purchase_token_required": "Android의 경우 purchase_token이 필요합니다",
"error.no_file_provided": "파일이 제공되지 않았습니다", "error.no_file_provided": "파일이 제공되지 않았습니다",
"error.failed_to_fetch_residence_types": "주거지 유형을 가져오는데 실패했습니다", "error.failed_to_fetch_residence_types": "주거지 유형을 가져오는데 실패했습니다",
"error.failed_to_fetch_task_categories": "작업 카테고리를 가져오는데 실패했습니다", "error.failed_to_fetch_task_categories": "작업 카테고리를 가져오는데 실패했습니다",
"error.failed_to_fetch_task_priorities": "작업 우선순위를 가져오는데 실패했습니다", "error.failed_to_fetch_task_priorities": "작업 우선순위를 가져오는데 실패했습니다",
"error.failed_to_fetch_task_frequencies": "작업 빈도를 가져오는데 실패했습니다", "error.failed_to_fetch_task_frequencies": "작업 빈도를 가져오는데 실패했습니다",
"error.failed_to_fetch_task_statuses": "작업 상태를 가져오는데 실패했습니다", "error.failed_to_fetch_task_statuses": "작업 상태를 가져오는데 실패했습니다",
"error.failed_to_fetch_contractor_specialties": "계약업체 전문 분야를 가져오는데 실패했습니다", "error.failed_to_fetch_contractor_specialties": "계약업체 전문 분야를 가져오는데 실패했습니다",
"push.task_due_soon.title": "작업 마감 임박", "push.task_due_soon.title": "작업 마감 임박",
"push.task_due_soon.body": "{{.TaskTitle}}의 마감일은 {{.DueDate}}입니다", "push.task_due_soon.body": "{{.TaskTitle}}의 마감일은 {{.DueDate}}입니다",
"push.task_overdue.title": "지연된 작업", "push.task_overdue.title": "지연된 작업",
@@ -129,23 +111,20 @@
"push.task_assigned.body": "{{.TaskTitle}}이(가) 할당되었습니다", "push.task_assigned.body": "{{.TaskTitle}}이(가) 할당되었습니다",
"push.residence_shared.title": "주거지 공유", "push.residence_shared.title": "주거지 공유",
"push.residence_shared.body": "{{.UserName}}님이 {{.ResidenceName}}을(를) 공유했습니다", "push.residence_shared.body": "{{.UserName}}님이 {{.ResidenceName}}을(를) 공유했습니다",
"email.welcome.subject": "honeyDue에 오신 것을 환영합니다!", "email.welcome.subject": "honeyDue에 오신 것을 환영합니다!",
"email.verification.subject": "이메일 인증", "email.verification.subject": "이메일 인증",
"email.password_reset.subject": "비밀번호 재설정 코드", "email.password_reset.subject": "비밀번호 재설정 코드",
"email.tasks_report.subject": "{{.ResidenceName}} 작업 보고서", "email.tasks_report.subject": "{{.ResidenceName}} 작업 보고서",
"lookup.residence_type.house": "주택",
"lookup.residence_type.house": "단독주택",
"lookup.residence_type.apartment": "아파트", "lookup.residence_type.apartment": "아파트",
"lookup.residence_type.condo": "콘도", "lookup.residence_type.condo": "콘도",
"lookup.residence_type.townhouse": "타운하우스", "lookup.residence_type.townhouse": "타운하우스",
"lookup.residence_type.mobile_home": "이동식 주택", "lookup.residence_type.mobile_home": "이동식 주택",
"lookup.residence_type.other": "기타", "lookup.residence_type.other": "기타",
"lookup.task_category.plumbing": "배관", "lookup.task_category.plumbing": "배관",
"lookup.task_category.electrical": "전기", "lookup.task_category.electrical": "전기",
"lookup.task_category.hvac": "냉난방", "lookup.task_category.hvac": "냉난방",
"lookup.task_category.appliances": "가전제품", "lookup.task_category.appliances": "가전",
"lookup.task_category.exterior": "외부", "lookup.task_category.exterior": "외부",
"lookup.task_category.interior": "내부", "lookup.task_category.interior": "내부",
"lookup.task_category.landscaping": "조경", "lookup.task_category.landscaping": "조경",
@@ -154,18 +133,15 @@
"lookup.task_category.pest_control": "해충 방제", "lookup.task_category.pest_control": "해충 방제",
"lookup.task_category.seasonal": "계절별", "lookup.task_category.seasonal": "계절별",
"lookup.task_category.other": "기타", "lookup.task_category.other": "기타",
"lookup.task_priority.low": "낮음", "lookup.task_priority.low": "낮음",
"lookup.task_priority.medium": "보통", "lookup.task_priority.medium": "보통",
"lookup.task_priority.high": "높음", "lookup.task_priority.high": "높음",
"lookup.task_priority.urgent": "긴급", "lookup.task_priority.urgent": "긴급",
"lookup.task_status.pending": "대기 중", "lookup.task_status.pending": "대기 중",
"lookup.task_status.in_progress": "진행 중", "lookup.task_status.in_progress": "진행 중",
"lookup.task_status.completed": "완료", "lookup.task_status.completed": "완료",
"lookup.task_status.cancelled": "취소됨", "lookup.task_status.cancelled": "취소됨",
"lookup.task_status.archived": "보관됨", "lookup.task_status.archived": "보관됨",
"lookup.task_frequency.once": "한 번", "lookup.task_frequency.once": "한 번",
"lookup.task_frequency.daily": "매일", "lookup.task_frequency.daily": "매일",
"lookup.task_frequency.weekly": "매주", "lookup.task_frequency.weekly": "매주",
@@ -174,18 +150,98 @@
"lookup.task_frequency.quarterly": "분기별", "lookup.task_frequency.quarterly": "분기별",
"lookup.task_frequency.semiannually": "6개월마다", "lookup.task_frequency.semiannually": "6개월마다",
"lookup.task_frequency.annually": "매년", "lookup.task_frequency.annually": "매년",
"lookup.contractor_specialty.plumber": "배관공", "lookup.contractor_specialty.plumber": "배관공",
"lookup.contractor_specialty.electrician": "전기 기사", "lookup.contractor_specialty.electrician": "전기 기사",
"lookup.contractor_specialty.hvac_technician": "냉난방 기사", "lookup.contractor_specialty.hvac_technician": "냉난방 기사",
"lookup.contractor_specialty.handyman": "리공", "lookup.contractor_specialty.handyman": "리공",
"lookup.contractor_specialty.landscaper": "조경사", "lookup.contractor_specialty.landscaper": "조경사",
"lookup.contractor_specialty.roofer": "지붕", "lookup.contractor_specialty.roofer": "지붕 기술자",
"lookup.contractor_specialty.painter": "도배공", "lookup.contractor_specialty.painter": "페인터",
"lookup.contractor_specialty.carpenter": "목수", "lookup.contractor_specialty.carpenter": "목수",
"lookup.contractor_specialty.pest_control": "해충 방제", "lookup.contractor_specialty.pest_control": "해충 방제",
"lookup.contractor_specialty.cleaning": "청소", "lookup.contractor_specialty.cleaning": "청소",
"lookup.contractor_specialty.pool_service": "수영장 관리", "lookup.contractor_specialty.pool_service": "수영장 서비스",
"lookup.contractor_specialty.general_contractor": "종합 건설업", "lookup.contractor_specialty.general_contractor": "종합 건설업",
"lookup.contractor_specialty.other": "기타" "lookup.contractor_specialty.other": "기타",
"suggestion.reason.has_pool": "집에 수영장이 있습니다",
"suggestion.reason.has_sprinkler_system": "집에 스프링클러 시스템이 있습니다",
"suggestion.reason.has_septic": "집에 정화조가 있습니다",
"suggestion.reason.has_fireplace": "집에 벽난로가 있습니다",
"suggestion.reason.has_garage": "집에 차고가 있습니다",
"suggestion.reason.has_basement": "집에 지하실이 있습니다",
"suggestion.reason.has_attic": "집에 다락방이 있습니다",
"suggestion.reason.heating_type": "난방 시스템과 일치합니다",
"suggestion.reason.cooling_type": "냉방 시스템과 일치합니다",
"suggestion.reason.water_heater_type": "온수기와 일치합니다",
"suggestion.reason.roof_type": "지붕과 일치합니다",
"suggestion.reason.exterior_type": "외장과 일치합니다",
"suggestion.reason.flooring_primary": "바닥재와 일치합니다",
"suggestion.reason.landscaping_type": "조경과 일치합니다",
"suggestion.reason.property_type": "주택 유형에 추천됩니다",
"suggestion.reason.climate_region": "거주 지역 기후에 추천됩니다",
"lookup.residence_type.duplex": "듀플렉스",
"lookup.residence_type.vacation_home": "별장",
"lookup.task_category.general": "일반",
"lookup.task_frequency.bi_weekly": "격주",
"lookup.task_frequency.semi_annually": "반기별",
"lookup.task_frequency.custom": "사용자 지정",
"lookup.contractor_specialty.appliance_repair": "가전 수리",
"lookup.contractor_specialty.cleaner": "청소부",
"lookup.contractor_specialty.locksmith": "열쇠공",
"lookup.home_profile.gas_furnace": "가스 난로",
"lookup.home_profile.electric_furnace": "전기 난로",
"lookup.home_profile.heat_pump": "열펌프",
"lookup.home_profile.boiler": "보일러",
"lookup.home_profile.radiant": "복사식",
"lookup.home_profile.other": "기타",
"lookup.home_profile.central_ac": "중앙 에어컨",
"lookup.home_profile.window_ac": "창문형 에어컨",
"lookup.home_profile.evaporative": "증발식",
"lookup.home_profile.none": "없음",
"lookup.home_profile.tank_gas": "탱크식(가스)",
"lookup.home_profile.tank_electric": "탱크식(전기)",
"lookup.home_profile.tankless_gas": "탱크리스(가스)",
"lookup.home_profile.tankless_electric": "탱크리스(전기)",
"lookup.home_profile.solar": "태양광",
"lookup.home_profile.asphalt_shingle": "아스팔트 슁글",
"lookup.home_profile.metal": "금속",
"lookup.home_profile.tile": "타일",
"lookup.home_profile.slate": "슬레이트",
"lookup.home_profile.wood_shake": "목재 셰이크",
"lookup.home_profile.flat": "평지붕",
"lookup.home_profile.brick": "벽돌",
"lookup.home_profile.vinyl_siding": "비닐 사이딩",
"lookup.home_profile.wood_siding": "목재 사이딩",
"lookup.home_profile.stucco": "스투코",
"lookup.home_profile.stone": "석재",
"lookup.home_profile.fiber_cement": "섬유 시멘트",
"lookup.home_profile.hardwood": "원목",
"lookup.home_profile.laminate": "라미네이트",
"lookup.home_profile.carpet": "카펫",
"lookup.home_profile.vinyl": "비닐",
"lookup.home_profile.concrete": "콘크리트",
"lookup.home_profile.lawn": "잔디",
"lookup.home_profile.desert": "사막",
"lookup.home_profile.xeriscape": "제리스케이프",
"lookup.home_profile.garden": "정원",
"lookup.home_profile.mixed": "혼합",
"lookup.document_type.warranty": "보증",
"lookup.document_type.manual": "사용 설명서",
"lookup.document_type.receipt": "영수증/송장",
"lookup.document_type.inspection": "점검 보고서",
"lookup.document_type.permit": "허가증",
"lookup.document_type.deed": "증서/권리증",
"lookup.document_type.insurance": "보험",
"lookup.document_type.contract": "계약",
"lookup.document_type.photo": "사진",
"lookup.document_type.other": "기타",
"lookup.document_category.appliance": "가전",
"lookup.document_category.hvac": "냉난방",
"lookup.document_category.plumbing": "배관",
"lookup.document_category.electrical": "전기",
"lookup.document_category.roofing": "지붕",
"lookup.document_category.structural": "구조",
"lookup.document_category.landscaping": "조경",
"lookup.document_category.general": "일반",
"lookup.document_category.other": "기타"
} }
+93 -37
View File
@@ -25,7 +25,6 @@
"error.google_signin_not_configured": "Google Sign In is niet geconfigureerd", "error.google_signin_not_configured": "Google Sign In is niet geconfigureerd",
"error.google_signin_failed": "Google Sign In mislukt", "error.google_signin_failed": "Google Sign In mislukt",
"error.invalid_google_token": "Ongeldig Google identiteitstoken", "error.invalid_google_token": "Ongeldig Google identiteitstoken",
"error.invalid_task_id": "Ongeldig taak-ID", "error.invalid_task_id": "Ongeldig taak-ID",
"error.invalid_residence_id": "Ongeldig woning-ID", "error.invalid_residence_id": "Ongeldig woning-ID",
"error.invalid_contractor_id": "Ongeldig aannemer-ID", "error.invalid_contractor_id": "Ongeldig aannemer-ID",
@@ -34,7 +33,6 @@
"error.invalid_user_id": "Ongeldig gebruikers-ID", "error.invalid_user_id": "Ongeldig gebruikers-ID",
"error.invalid_notification_id": "Ongeldig notificatie-ID", "error.invalid_notification_id": "Ongeldig notificatie-ID",
"error.invalid_device_id": "Ongeldig apparaat-ID", "error.invalid_device_id": "Ongeldig apparaat-ID",
"error.task_not_found": "Taak niet gevonden", "error.task_not_found": "Taak niet gevonden",
"error.residence_not_found": "Woning niet gevonden", "error.residence_not_found": "Woning niet gevonden",
"error.contractor_not_found": "Aannemer niet gevonden", "error.contractor_not_found": "Aannemer niet gevonden",
@@ -43,7 +41,6 @@
"error.user_not_found": "Gebruiker niet gevonden", "error.user_not_found": "Gebruiker niet gevonden",
"error.share_code_invalid": "Ongeldige deelcode", "error.share_code_invalid": "Ongeldige deelcode",
"error.share_code_expired": "Deelcode is verlopen", "error.share_code_expired": "Deelcode is verlopen",
"error.task_access_denied": "U heeft geen toegang tot deze taak", "error.task_access_denied": "U heeft geen toegang tot deze taak",
"error.residence_access_denied": "U heeft geen toegang tot deze woning", "error.residence_access_denied": "U heeft geen toegang tot deze woning",
"error.contractor_access_denied": "U heeft geen toegang tot deze aannemer", "error.contractor_access_denied": "U heeft geen toegang tot deze aannemer",
@@ -52,10 +49,8 @@
"error.cannot_remove_owner": "Kan de woningeigenaar niet verwijderen", "error.cannot_remove_owner": "Kan de woningeigenaar niet verwijderen",
"error.user_already_member": "Gebruiker is al lid van deze woning", "error.user_already_member": "Gebruiker is al lid van deze woning",
"error.properties_limit_reached": "U heeft het maximale aantal woningen voor uw abonnement bereikt", "error.properties_limit_reached": "U heeft het maximale aantal woningen voor uw abonnement bereikt",
"error.task_already_cancelled": "Taak is al geannuleerd", "error.task_already_cancelled": "Taak is al geannuleerd",
"error.task_already_archived": "Taak is al gearchiveerd", "error.task_already_archived": "Taak is al gearchiveerd",
"error.failed_to_parse_form": "Multipart formulier parsen mislukt", "error.failed_to_parse_form": "Multipart formulier parsen mislukt",
"error.task_id_required": "task_id is verplicht", "error.task_id_required": "task_id is verplicht",
"error.invalid_task_id_value": "Ongeldig task_id", "error.invalid_task_id_value": "Ongeldig task_id",
@@ -64,14 +59,12 @@
"error.invalid_residence_id_value": "Ongeldig residence_id", "error.invalid_residence_id_value": "Ongeldig residence_id",
"error.title_required": "titel is verplicht", "error.title_required": "titel is verplicht",
"error.failed_to_upload_file": "Bestand uploaden mislukt", "error.failed_to_upload_file": "Bestand uploaden mislukt",
"message.logged_out": "Succesvol uitgelogd", "message.logged_out": "Succesvol uitgelogd",
"message.email_verified": "E-mailadres succesvol geverifieerd", "message.email_verified": "E-mailadres succesvol geverifieerd",
"message.verification_email_sent": "Verificatie e-mail verzonden", "message.verification_email_sent": "Verificatie e-mail verzonden",
"message.password_reset_email_sent": "Als er een account met dat e-mailadres bestaat, is er een wachtwoord resetcode verzonden.", "message.password_reset_email_sent": "Als er een account met dat e-mailadres bestaat, is er een wachtwoord resetcode verzonden.",
"message.reset_code_verified": "Code succesvol geverifieerd", "message.reset_code_verified": "Code succesvol geverifieerd",
"message.password_reset_success": "Wachtwoord succesvol gereset. Log in met uw nieuwe wachtwoord.", "message.password_reset_success": "Wachtwoord succesvol gereset. Log in met uw nieuwe wachtwoord.",
"message.task_deleted": "Taak succesvol verwijderd", "message.task_deleted": "Taak succesvol verwijderd",
"message.task_in_progress": "Taak gemarkeerd als in uitvoering", "message.task_in_progress": "Taak gemarkeerd als in uitvoering",
"message.task_cancelled": "Taak geannuleerd", "message.task_cancelled": "Taak geannuleerd",
@@ -79,46 +72,35 @@
"message.task_archived": "Taak gearchiveerd", "message.task_archived": "Taak gearchiveerd",
"message.task_unarchived": "Taak gearchiveerd ongedaan gemaakt", "message.task_unarchived": "Taak gearchiveerd ongedaan gemaakt",
"message.completion_deleted": "Voltooiing succesvol verwijderd", "message.completion_deleted": "Voltooiing succesvol verwijderd",
"message.residence_deleted": "Woning succesvol verwijderd", "message.residence_deleted": "Woning succesvol verwijderd",
"message.user_removed": "Gebruiker verwijderd van woning", "message.user_removed": "Gebruiker verwijderd van woning",
"message.tasks_report_generated": "Takenrapport succesvol gegenereerd", "message.tasks_report_generated": "Takenrapport succesvol gegenereerd",
"message.tasks_report_sent": "Takenrapport gegenereerd en verzonden naar {{.Email}}", "message.tasks_report_sent": "Takenrapport gegenereerd en verzonden naar {{.Email}}",
"message.tasks_report_email_failed": "Takenrapport gegenereerd maar e-mail kon niet worden verzonden", "message.tasks_report_email_failed": "Takenrapport gegenereerd maar e-mail kon niet worden verzonden",
"message.contractor_deleted": "Aannemer succesvol verwijderd", "message.contractor_deleted": "Aannemer succesvol verwijderd",
"message.document_deleted": "Document succesvol verwijderd", "message.document_deleted": "Document succesvol verwijderd",
"message.document_activated": "Document geactiveerd", "message.document_activated": "Document geactiveerd",
"message.document_deactivated": "Document gedeactiveerd", "message.document_deactivated": "Document gedeactiveerd",
"message.notification_marked_read": "Notificatie gemarkeerd als gelezen", "message.notification_marked_read": "Notificatie gemarkeerd als gelezen",
"message.all_notifications_marked_read": "Alle notificaties gemarkeerd als gelezen", "message.all_notifications_marked_read": "Alle notificaties gemarkeerd als gelezen",
"message.device_removed": "Apparaat verwijderd", "message.device_removed": "Apparaat verwijderd",
"message.subscription_upgraded": "Abonnement succesvol geüpgraded", "message.subscription_upgraded": "Abonnement succesvol geüpgraded",
"message.subscription_cancelled": "Abonnement geannuleerd. U behoudt Pro voordelen tot het einde van uw factureringsperiode.", "message.subscription_cancelled": "Abonnement geannuleerd. U behoudt Pro voordelen tot het einde van uw factureringsperiode.",
"message.subscription_restored": "Abonnement succesvol hersteld", "message.subscription_restored": "Abonnement succesvol hersteld",
"message.file_deleted": "Bestand succesvol verwijderd", "message.file_deleted": "Bestand succesvol verwijderd",
"message.static_data_refreshed": "Statische gegevens vernieuwd", "message.static_data_refreshed": "Statische gegevens vernieuwd",
"error.notification_not_found": "Notificatie niet gevonden", "error.notification_not_found": "Notificatie niet gevonden",
"error.invalid_platform": "Ongeldig platform", "error.invalid_platform": "Ongeldig platform",
"error.upgrade_trigger_not_found": "Upgrade trigger niet gevonden", "error.upgrade_trigger_not_found": "Upgrade trigger niet gevonden",
"error.receipt_data_required": "receipt_data is verplicht voor iOS", "error.receipt_data_required": "receipt_data is verplicht voor iOS",
"error.purchase_token_required": "purchase_token is verplicht voor Android", "error.purchase_token_required": "purchase_token is verplicht voor Android",
"error.no_file_provided": "Geen bestand aangeleverd", "error.no_file_provided": "Geen bestand aangeleverd",
"error.failed_to_fetch_residence_types": "Woningtypes ophalen mislukt", "error.failed_to_fetch_residence_types": "Woningtypes ophalen mislukt",
"error.failed_to_fetch_task_categories": "Taakcategorieën ophalen mislukt", "error.failed_to_fetch_task_categories": "Taakcategorieën ophalen mislukt",
"error.failed_to_fetch_task_priorities": "Taakprioriteiten ophalen mislukt", "error.failed_to_fetch_task_priorities": "Taakprioriteiten ophalen mislukt",
"error.failed_to_fetch_task_frequencies": "Taakfrequenties ophalen mislukt", "error.failed_to_fetch_task_frequencies": "Taakfrequenties ophalen mislukt",
"error.failed_to_fetch_task_statuses": "Taakstatussen ophalen mislukt", "error.failed_to_fetch_task_statuses": "Taakstatussen ophalen mislukt",
"error.failed_to_fetch_contractor_specialties": "Aannemer specialiteiten ophalen mislukt", "error.failed_to_fetch_contractor_specialties": "Aannemer specialiteiten ophalen mislukt",
"push.task_due_soon.title": "Taak Vervalt Binnenkort", "push.task_due_soon.title": "Taak Vervalt Binnenkort",
"push.task_due_soon.body": "{{.TaskTitle}} vervalt {{.DueDate}}", "push.task_due_soon.body": "{{.TaskTitle}} vervalt {{.DueDate}}",
"push.task_overdue.title": "Verlopen Taak", "push.task_overdue.title": "Verlopen Taak",
@@ -129,55 +111,48 @@
"push.task_assigned.body": "U bent toegewezen aan {{.TaskTitle}}", "push.task_assigned.body": "U bent toegewezen aan {{.TaskTitle}}",
"push.residence_shared.title": "Woning Gedeeld", "push.residence_shared.title": "Woning Gedeeld",
"push.residence_shared.body": "{{.UserName}} heeft {{.ResidenceName}} met u gedeeld", "push.residence_shared.body": "{{.UserName}} heeft {{.ResidenceName}} met u gedeeld",
"email.welcome.subject": "Welkom bij honeyDue!", "email.welcome.subject": "Welkom bij honeyDue!",
"email.verification.subject": "Verifieer Uw E-mailadres", "email.verification.subject": "Verifieer Uw E-mailadres",
"email.password_reset.subject": "Wachtwoord Resetcode", "email.password_reset.subject": "Wachtwoord Resetcode",
"email.tasks_report.subject": "Takenrapport voor {{.ResidenceName}}", "email.tasks_report.subject": "Takenrapport voor {{.ResidenceName}}",
"lookup.residence_type.house": "Huis", "lookup.residence_type.house": "Huis",
"lookup.residence_type.apartment": "Appartement", "lookup.residence_type.apartment": "Appartement",
"lookup.residence_type.condo": "Appartement met eigen grond", "lookup.residence_type.condo": "Koopflat",
"lookup.residence_type.townhouse": "Rijtjeshuis", "lookup.residence_type.townhouse": "Rijtjeshuis",
"lookup.residence_type.mobile_home": "Stacaravan", "lookup.residence_type.mobile_home": "Stacaravan",
"lookup.residence_type.other": "Anders", "lookup.residence_type.other": "Overig",
"lookup.task_category.plumbing": "Loodgieterswerk",
"lookup.task_category.plumbing": "Loodgieterij", "lookup.task_category.electrical": "Elektrisch",
"lookup.task_category.electrical": "Elektriciteit", "lookup.task_category.hvac": "HVAC",
"lookup.task_category.hvac": "Verwarming en Ventilatie",
"lookup.task_category.appliances": "Apparaten", "lookup.task_category.appliances": "Apparaten",
"lookup.task_category.exterior": "Buitenkant", "lookup.task_category.exterior": "Buiten",
"lookup.task_category.interior": "Binnenkant", "lookup.task_category.interior": "Binnen",
"lookup.task_category.landscaping": "Tuinonderhoud", "lookup.task_category.landscaping": "Tuinonderhoud",
"lookup.task_category.safety": "Veiligheid", "lookup.task_category.safety": "Veiligheid",
"lookup.task_category.cleaning": "Schoonmaak", "lookup.task_category.cleaning": "Schoonmaak",
"lookup.task_category.pest_control": "Ongediertebestrijding", "lookup.task_category.pest_control": "Ongediertebestrijding",
"lookup.task_category.seasonal": "Seizoensgebonden", "lookup.task_category.seasonal": "Seizoensgebonden",
"lookup.task_category.other": "Anders", "lookup.task_category.other": "Anders",
"lookup.task_priority.low": "Laag", "lookup.task_priority.low": "Laag",
"lookup.task_priority.medium": "Gemiddeld", "lookup.task_priority.medium": "Gemiddeld",
"lookup.task_priority.high": "Hoog", "lookup.task_priority.high": "Hoog",
"lookup.task_priority.urgent": "Urgent", "lookup.task_priority.urgent": "Urgent",
"lookup.task_status.pending": "In afwachting", "lookup.task_status.pending": "In afwachting",
"lookup.task_status.in_progress": "In uitvoering", "lookup.task_status.in_progress": "In uitvoering",
"lookup.task_status.completed": "Voltooid", "lookup.task_status.completed": "Voltooid",
"lookup.task_status.cancelled": "Geannuleerd", "lookup.task_status.cancelled": "Geannuleerd",
"lookup.task_status.archived": "Gearchiveerd", "lookup.task_status.archived": "Gearchiveerd",
"lookup.task_frequency.once": "Eenmalig", "lookup.task_frequency.once": "Eenmalig",
"lookup.task_frequency.daily": "Dagelijks", "lookup.task_frequency.daily": "Dagelijks",
"lookup.task_frequency.weekly": "Wekelijks", "lookup.task_frequency.weekly": "Wekelijks",
"lookup.task_frequency.biweekly": "Om de 2 Weken", "lookup.task_frequency.biweekly": "Om de 2 Weken",
"lookup.task_frequency.monthly": "Maandelijks", "lookup.task_frequency.monthly": "Maandelijks",
"lookup.task_frequency.quarterly": "Per Kwartaal", "lookup.task_frequency.quarterly": "Per kwartaal",
"lookup.task_frequency.semiannually": "Om de 6 Maanden", "lookup.task_frequency.semiannually": "Om de 6 Maanden",
"lookup.task_frequency.annually": "Jaarlijks", "lookup.task_frequency.annually": "Jaarlijks",
"lookup.contractor_specialty.plumber": "Loodgieter", "lookup.contractor_specialty.plumber": "Loodgieter",
"lookup.contractor_specialty.electrician": "Elektricien", "lookup.contractor_specialty.electrician": "Elektricien",
"lookup.contractor_specialty.hvac_technician": "HVAC Monteur", "lookup.contractor_specialty.hvac_technician": "HVAC-technicus",
"lookup.contractor_specialty.handyman": "Klusjesman", "lookup.contractor_specialty.handyman": "Klusjesman",
"lookup.contractor_specialty.landscaper": "Hovenier", "lookup.contractor_specialty.landscaper": "Hovenier",
"lookup.contractor_specialty.roofer": "Dakdekker", "lookup.contractor_specialty.roofer": "Dakdekker",
@@ -185,7 +160,88 @@
"lookup.contractor_specialty.carpenter": "Timmerman", "lookup.contractor_specialty.carpenter": "Timmerman",
"lookup.contractor_specialty.pest_control": "Ongediertebestrijding", "lookup.contractor_specialty.pest_control": "Ongediertebestrijding",
"lookup.contractor_specialty.cleaning": "Schoonmaak", "lookup.contractor_specialty.cleaning": "Schoonmaak",
"lookup.contractor_specialty.pool_service": "Zwembadonderhoud", "lookup.contractor_specialty.pool_service": "Zwembadservice",
"lookup.contractor_specialty.general_contractor": "Algemeen Aannemer", "lookup.contractor_specialty.general_contractor": "Hoofdaannemer",
"lookup.contractor_specialty.other": "Anders" "lookup.contractor_specialty.other": "Anders",
"suggestion.reason.has_pool": "Je woning heeft een zwembad",
"suggestion.reason.has_sprinkler_system": "Je woning heeft een sproei-installatie",
"suggestion.reason.has_septic": "Je woning heeft een septische tank",
"suggestion.reason.has_fireplace": "Je woning heeft een open haard",
"suggestion.reason.has_garage": "Je woning heeft een garage",
"suggestion.reason.has_basement": "Je woning heeft een kelder",
"suggestion.reason.has_attic": "Je woning heeft een zolder",
"suggestion.reason.heating_type": "Past bij je verwarmingssysteem",
"suggestion.reason.cooling_type": "Past bij je koelsysteem",
"suggestion.reason.water_heater_type": "Past bij je boiler",
"suggestion.reason.roof_type": "Past bij je dak",
"suggestion.reason.exterior_type": "Past bij je gevel",
"suggestion.reason.flooring_primary": "Past bij je vloer",
"suggestion.reason.landscaping_type": "Past bij je tuin",
"suggestion.reason.property_type": "Aanbevolen voor je type woning",
"suggestion.reason.climate_region": "Aanbevolen voor je klimaat",
"lookup.residence_type.duplex": "Twee-onder-een-kap",
"lookup.residence_type.vacation_home": "Vakantiehuis",
"lookup.task_category.general": "Algemeen",
"lookup.task_frequency.bi_weekly": "Tweewekelijks",
"lookup.task_frequency.semi_annually": "Halfjaarlijks",
"lookup.task_frequency.custom": "Aangepast",
"lookup.contractor_specialty.appliance_repair": "Apparaatreparatie",
"lookup.contractor_specialty.cleaner": "Schoonmaker",
"lookup.contractor_specialty.locksmith": "Slotenmaker",
"lookup.home_profile.gas_furnace": "Gasketel",
"lookup.home_profile.electric_furnace": "Elektrische ketel",
"lookup.home_profile.heat_pump": "Warmtepomp",
"lookup.home_profile.boiler": "CV-ketel",
"lookup.home_profile.radiant": "Stralingsverwarming",
"lookup.home_profile.other": "Overig",
"lookup.home_profile.central_ac": "Centrale airco",
"lookup.home_profile.window_ac": "Raamairco",
"lookup.home_profile.evaporative": "Verdampings",
"lookup.home_profile.none": "Geen",
"lookup.home_profile.tank_gas": "Boiler (gas)",
"lookup.home_profile.tank_electric": "Boiler (elektrisch)",
"lookup.home_profile.tankless_gas": "Doorstroom (gas)",
"lookup.home_profile.tankless_electric": "Doorstroom (elektrisch)",
"lookup.home_profile.solar": "Zonne-energie",
"lookup.home_profile.asphalt_shingle": "Asfaltshingle",
"lookup.home_profile.metal": "Metaal",
"lookup.home_profile.tile": "Dakpan",
"lookup.home_profile.slate": "Leisteen",
"lookup.home_profile.wood_shake": "Houten shingle",
"lookup.home_profile.flat": "Plat",
"lookup.home_profile.brick": "Baksteen",
"lookup.home_profile.vinyl_siding": "Vinyl gevelbekleding",
"lookup.home_profile.wood_siding": "Houten gevelbekleding",
"lookup.home_profile.stucco": "Stucwerk",
"lookup.home_profile.stone": "Steen",
"lookup.home_profile.fiber_cement": "Vezelcement",
"lookup.home_profile.hardwood": "Hardhout",
"lookup.home_profile.laminate": "Laminaat",
"lookup.home_profile.carpet": "Tapijt",
"lookup.home_profile.vinyl": "Vinyl",
"lookup.home_profile.concrete": "Beton",
"lookup.home_profile.lawn": "Gazon",
"lookup.home_profile.desert": "Woestijn",
"lookup.home_profile.xeriscape": "Xeriscaping",
"lookup.home_profile.garden": "Tuin",
"lookup.home_profile.mixed": "Gemengd",
"lookup.document_type.warranty": "Garantie",
"lookup.document_type.manual": "Handleiding",
"lookup.document_type.receipt": "Bon/Factuur",
"lookup.document_type.inspection": "Inspectierapport",
"lookup.document_type.permit": "Vergunning",
"lookup.document_type.deed": "Akte/Eigendomsbewijs",
"lookup.document_type.insurance": "Verzekering",
"lookup.document_type.contract": "Contract",
"lookup.document_type.photo": "Foto",
"lookup.document_type.other": "Overig",
"lookup.document_category.appliance": "Apparaat",
"lookup.document_category.hvac": "HVAC",
"lookup.document_category.plumbing": "Loodgieterswerk",
"lookup.document_category.electrical": "Elektrisch",
"lookup.document_category.roofing": "Dak",
"lookup.document_category.structural": "Constructie",
"lookup.document_category.landscaping": "Tuinaanleg",
"lookup.document_category.general": "Algemeen",
"lookup.document_category.other": "Overig"
} }
+97 -41
View File
@@ -25,7 +25,6 @@
"error.google_signin_not_configured": "O login com Google nao esta configurado", "error.google_signin_not_configured": "O login com Google nao esta configurado",
"error.google_signin_failed": "Falha no login com Google", "error.google_signin_failed": "Falha no login com Google",
"error.invalid_google_token": "Token de identidade Google invalido", "error.invalid_google_token": "Token de identidade Google invalido",
"error.invalid_task_id": "ID da tarefa invalido", "error.invalid_task_id": "ID da tarefa invalido",
"error.invalid_residence_id": "ID da propriedade invalido", "error.invalid_residence_id": "ID da propriedade invalido",
"error.invalid_contractor_id": "ID do prestador invalido", "error.invalid_contractor_id": "ID do prestador invalido",
@@ -34,7 +33,6 @@
"error.invalid_user_id": "ID do usuario invalido", "error.invalid_user_id": "ID do usuario invalido",
"error.invalid_notification_id": "ID da notificacao invalido", "error.invalid_notification_id": "ID da notificacao invalido",
"error.invalid_device_id": "ID do dispositivo invalido", "error.invalid_device_id": "ID do dispositivo invalido",
"error.task_not_found": "Tarefa nao encontrada", "error.task_not_found": "Tarefa nao encontrada",
"error.residence_not_found": "Propriedade nao encontrada", "error.residence_not_found": "Propriedade nao encontrada",
"error.contractor_not_found": "Prestador nao encontrado", "error.contractor_not_found": "Prestador nao encontrado",
@@ -43,7 +41,6 @@
"error.user_not_found": "Usuario nao encontrado", "error.user_not_found": "Usuario nao encontrado",
"error.share_code_invalid": "Codigo de compartilhamento invalido", "error.share_code_invalid": "Codigo de compartilhamento invalido",
"error.share_code_expired": "O codigo de compartilhamento expirou", "error.share_code_expired": "O codigo de compartilhamento expirou",
"error.task_access_denied": "Voce nao tem acesso a esta tarefa", "error.task_access_denied": "Voce nao tem acesso a esta tarefa",
"error.residence_access_denied": "Voce nao tem acesso a esta propriedade", "error.residence_access_denied": "Voce nao tem acesso a esta propriedade",
"error.contractor_access_denied": "Voce nao tem acesso a este prestador", "error.contractor_access_denied": "Voce nao tem acesso a este prestador",
@@ -52,10 +49,8 @@
"error.cannot_remove_owner": "Nao e possivel remover o proprietario", "error.cannot_remove_owner": "Nao e possivel remover o proprietario",
"error.user_already_member": "O usuario ja e membro desta propriedade", "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.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_cancelled": "A tarefa ja esta cancelada",
"error.task_already_archived": "A tarefa ja esta arquivada", "error.task_already_archived": "A tarefa ja esta arquivada",
"error.failed_to_parse_form": "Falha ao analisar o formulario", "error.failed_to_parse_form": "Falha ao analisar o formulario",
"error.task_id_required": "task_id e obrigatorio", "error.task_id_required": "task_id e obrigatorio",
"error.invalid_task_id_value": "task_id invalido", "error.invalid_task_id_value": "task_id invalido",
@@ -64,14 +59,12 @@
"error.invalid_residence_id_value": "residence_id invalido", "error.invalid_residence_id_value": "residence_id invalido",
"error.title_required": "Titulo e obrigatorio", "error.title_required": "Titulo e obrigatorio",
"error.failed_to_upload_file": "Falha ao enviar arquivo", "error.failed_to_upload_file": "Falha ao enviar arquivo",
"message.logged_out": "Logout realizado com sucesso", "message.logged_out": "Logout realizado com sucesso",
"message.email_verified": "Email verificado com sucesso", "message.email_verified": "Email verificado com sucesso",
"message.verification_email_sent": "Email de verificacao enviado", "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.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.reset_code_verified": "Codigo verificado com sucesso",
"message.password_reset_success": "Senha redefinida com sucesso. Por favor, faca login com sua nova senha.", "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_deleted": "Tarefa excluida com sucesso",
"message.task_in_progress": "Tarefa marcada como em andamento", "message.task_in_progress": "Tarefa marcada como em andamento",
"message.task_cancelled": "Tarefa cancelada", "message.task_cancelled": "Tarefa cancelada",
@@ -79,46 +72,35 @@
"message.task_archived": "Tarefa arquivada", "message.task_archived": "Tarefa arquivada",
"message.task_unarchived": "Tarefa desarquivada", "message.task_unarchived": "Tarefa desarquivada",
"message.completion_deleted": "Conclusao excluida com sucesso", "message.completion_deleted": "Conclusao excluida com sucesso",
"message.residence_deleted": "Propriedade excluida com sucesso", "message.residence_deleted": "Propriedade excluida com sucesso",
"message.user_removed": "Usuario removido da propriedade", "message.user_removed": "Usuario removido da propriedade",
"message.tasks_report_generated": "Relatorio de tarefas gerado com sucesso", "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_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.tasks_report_email_failed": "Relatorio de tarefas gerado mas o email nao pode ser enviado",
"message.contractor_deleted": "Prestador excluido com sucesso", "message.contractor_deleted": "Prestador excluido com sucesso",
"message.document_deleted": "Documento excluido com sucesso", "message.document_deleted": "Documento excluido com sucesso",
"message.document_activated": "Documento ativado", "message.document_activated": "Documento ativado",
"message.document_deactivated": "Documento desativado", "message.document_deactivated": "Documento desativado",
"message.notification_marked_read": "Notificação marcada como lida", "message.notification_marked_read": "Notificação marcada como lida",
"message.all_notifications_marked_read": "Todas as notificações marcadas como lidas", "message.all_notifications_marked_read": "Todas as notificações marcadas como lidas",
"message.device_removed": "Dispositivo removido", "message.device_removed": "Dispositivo removido",
"message.subscription_upgraded": "Assinatura atualizada com sucesso", "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_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.subscription_restored": "Assinatura restaurada com sucesso",
"message.file_deleted": "Arquivo excluído com sucesso", "message.file_deleted": "Arquivo excluído com sucesso",
"message.static_data_refreshed": "Dados estáticos atualizados", "message.static_data_refreshed": "Dados estáticos atualizados",
"error.notification_not_found": "Notificação não encontrada", "error.notification_not_found": "Notificação não encontrada",
"error.invalid_platform": "Plataforma inválida", "error.invalid_platform": "Plataforma inválida",
"error.upgrade_trigger_not_found": "Gatilho de atualização não encontrado", "error.upgrade_trigger_not_found": "Gatilho de atualização não encontrado",
"error.receipt_data_required": "receipt_data é obrigatório para iOS", "error.receipt_data_required": "receipt_data é obrigatório para iOS",
"error.purchase_token_required": "purchase_token é obrigatório para Android", "error.purchase_token_required": "purchase_token é obrigatório para Android",
"error.no_file_provided": "Nenhum arquivo fornecido", "error.no_file_provided": "Nenhum arquivo fornecido",
"error.failed_to_fetch_residence_types": "Falha ao buscar tipos de propriedade", "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_categories": "Falha ao buscar categorias de tarefas",
"error.failed_to_fetch_task_priorities": "Falha ao buscar prioridades 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_frequencies": "Falha ao buscar frequências de tarefas",
"error.failed_to_fetch_task_statuses": "Falha ao buscar status 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", "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.title": "Tarefa Proxima do Vencimento",
"push.task_due_soon.body": "{{.TaskTitle}} vence em {{.DueDate}}", "push.task_due_soon.body": "{{.TaskTitle}} vence em {{.DueDate}}",
"push.task_overdue.title": "Tarefa Atrasada", "push.task_overdue.title": "Tarefa Atrasada",
@@ -129,63 +111,137 @@
"push.task_assigned.body": "{{.TaskTitle}} foi atribuida a voce", "push.task_assigned.body": "{{.TaskTitle}} foi atribuida a voce",
"push.residence_shared.title": "Propriedade Compartilhada", "push.residence_shared.title": "Propriedade Compartilhada",
"push.residence_shared.body": "{{.UserName}} compartilhou {{.ResidenceName}} com voce", "push.residence_shared.body": "{{.UserName}} compartilhou {{.ResidenceName}} com voce",
"email.welcome.subject": "Bem-vindo ao honeyDue!", "email.welcome.subject": "Bem-vindo ao honeyDue!",
"email.verification.subject": "Verifique Seu Email", "email.verification.subject": "Verifique Seu Email",
"email.password_reset.subject": "Codigo de Redefinicao de Senha", "email.password_reset.subject": "Codigo de Redefinicao de Senha",
"email.tasks_report.subject": "Relatorio de Tarefas para {{.ResidenceName}}", "email.tasks_report.subject": "Relatorio de Tarefas para {{.ResidenceName}}",
"lookup.residence_type.house": "Casa", "lookup.residence_type.house": "Casa",
"lookup.residence_type.apartment": "Apartamento", "lookup.residence_type.apartment": "Apartamento",
"lookup.residence_type.condo": "Condominio", "lookup.residence_type.condo": "Condomínio",
"lookup.residence_type.townhouse": "Sobrado", "lookup.residence_type.townhouse": "Sobrado",
"lookup.residence_type.mobile_home": "Casa Movel", "lookup.residence_type.mobile_home": "Casa vel",
"lookup.residence_type.other": "Outro", "lookup.residence_type.other": "Outro",
"lookup.task_category.plumbing": "Encanamento", "lookup.task_category.plumbing": "Encanamento",
"lookup.task_category.electrical": "Eletrica", "lookup.task_category.electrical": "Elétrica",
"lookup.task_category.hvac": "Climatizacao", "lookup.task_category.hvac": "AVAC",
"lookup.task_category.appliances": "Eletrodomesticos", "lookup.task_category.appliances": "Eletrodomésticos",
"lookup.task_category.exterior": "Exterior", "lookup.task_category.exterior": "Exterior",
"lookup.task_category.interior": "Interior", "lookup.task_category.interior": "Interior",
"lookup.task_category.landscaping": "Paisagismo", "lookup.task_category.landscaping": "Paisagismo",
"lookup.task_category.safety": "Seguranca", "lookup.task_category.safety": "Segurança",
"lookup.task_category.cleaning": "Limpeza", "lookup.task_category.cleaning": "Limpeza",
"lookup.task_category.pest_control": "Controle de Pragas", "lookup.task_category.pest_control": "Controle de pragas",
"lookup.task_category.seasonal": "Sazonal", "lookup.task_category.seasonal": "Sazonal",
"lookup.task_category.other": "Outro", "lookup.task_category.other": "Outro",
"lookup.task_priority.low": "Baixa", "lookup.task_priority.low": "Baixa",
"lookup.task_priority.medium": "Media", "lookup.task_priority.medium": "Média",
"lookup.task_priority.high": "Alta", "lookup.task_priority.high": "Alta",
"lookup.task_priority.urgent": "Urgente", "lookup.task_priority.urgent": "Urgente",
"lookup.task_status.pending": "Pendente", "lookup.task_status.pending": "Pendente",
"lookup.task_status.in_progress": "Em Andamento", "lookup.task_status.in_progress": "Em Andamento",
"lookup.task_status.completed": "Concluida", "lookup.task_status.completed": "Concluida",
"lookup.task_status.cancelled": "Cancelada", "lookup.task_status.cancelled": "Cancelada",
"lookup.task_status.archived": "Arquivada", "lookup.task_status.archived": "Arquivada",
"lookup.task_frequency.once": "Uma vez",
"lookup.task_frequency.once": "Uma Vez", "lookup.task_frequency.daily": "Diário",
"lookup.task_frequency.daily": "Diario",
"lookup.task_frequency.weekly": "Semanal", "lookup.task_frequency.weekly": "Semanal",
"lookup.task_frequency.biweekly": "Quinzenal", "lookup.task_frequency.biweekly": "Quinzenal",
"lookup.task_frequency.monthly": "Mensal", "lookup.task_frequency.monthly": "Mensal",
"lookup.task_frequency.quarterly": "Trimestral", "lookup.task_frequency.quarterly": "Trimestral",
"lookup.task_frequency.semiannually": "Semestral", "lookup.task_frequency.semiannually": "Semestral",
"lookup.task_frequency.annually": "Anual", "lookup.task_frequency.annually": "Anual",
"lookup.contractor_specialty.plumber": "Encanador", "lookup.contractor_specialty.plumber": "Encanador",
"lookup.contractor_specialty.electrician": "Eletricista", "lookup.contractor_specialty.electrician": "Eletricista",
"lookup.contractor_specialty.hvac_technician": "Tecnico de Climatizacao", "lookup.contractor_specialty.hvac_technician": "Técnico de AVAC",
"lookup.contractor_specialty.handyman": "Faz-tudo", "lookup.contractor_specialty.handyman": "Faz-tudo",
"lookup.contractor_specialty.landscaper": "Paisagista", "lookup.contractor_specialty.landscaper": "Jardineiro",
"lookup.contractor_specialty.roofer": "Telhadista", "lookup.contractor_specialty.roofer": "Telhadista",
"lookup.contractor_specialty.painter": "Pintor", "lookup.contractor_specialty.painter": "Pintor",
"lookup.contractor_specialty.carpenter": "Carpinteiro", "lookup.contractor_specialty.carpenter": "Carpinteiro",
"lookup.contractor_specialty.pest_control": "Controle de Pragas", "lookup.contractor_specialty.pest_control": "Controle de pragas",
"lookup.contractor_specialty.cleaning": "Limpeza", "lookup.contractor_specialty.cleaning": "Limpeza",
"lookup.contractor_specialty.pool_service": "Servico de Piscina", "lookup.contractor_specialty.pool_service": "Serviço de piscina",
"lookup.contractor_specialty.general_contractor": "Empreiteiro Geral", "lookup.contractor_specialty.general_contractor": "Empreiteiro geral",
"lookup.contractor_specialty.other": "Outro" "lookup.contractor_specialty.other": "Outro",
"suggestion.reason.has_pool": "Sua casa tem piscina",
"suggestion.reason.has_sprinkler_system": "Sua casa tem sistema de irrigação",
"suggestion.reason.has_septic": "Sua casa tem fossa séptica",
"suggestion.reason.has_fireplace": "Sua casa tem lareira",
"suggestion.reason.has_garage": "Sua casa tem garagem",
"suggestion.reason.has_basement": "Sua casa tem porão",
"suggestion.reason.has_attic": "Sua casa tem sótão",
"suggestion.reason.heating_type": "Combina com seu sistema de aquecimento",
"suggestion.reason.cooling_type": "Combina com seu sistema de refrigeração",
"suggestion.reason.water_heater_type": "Combina com seu aquecedor de água",
"suggestion.reason.roof_type": "Combina com seu telhado",
"suggestion.reason.exterior_type": "Combina com seu exterior",
"suggestion.reason.flooring_primary": "Combina com seu piso",
"suggestion.reason.landscaping_type": "Combina com seu paisagismo",
"suggestion.reason.property_type": "Recomendado para seu tipo de imóvel",
"suggestion.reason.climate_region": "Recomendado para seu clima",
"lookup.residence_type.duplex": "Duplex",
"lookup.residence_type.vacation_home": "Casa de férias",
"lookup.task_category.general": "Geral",
"lookup.task_frequency.bi_weekly": "Quinzenal",
"lookup.task_frequency.semi_annually": "Semestral",
"lookup.task_frequency.custom": "Personalizado",
"lookup.contractor_specialty.appliance_repair": "Reparo de eletrodomésticos",
"lookup.contractor_specialty.cleaner": "Faxineiro",
"lookup.contractor_specialty.locksmith": "Chaveiro",
"lookup.home_profile.gas_furnace": "Aquecedor a gás",
"lookup.home_profile.electric_furnace": "Aquecedor elétrico",
"lookup.home_profile.heat_pump": "Bomba de calor",
"lookup.home_profile.boiler": "Caldeira",
"lookup.home_profile.radiant": "Radiante",
"lookup.home_profile.other": "Outro",
"lookup.home_profile.central_ac": "AC central",
"lookup.home_profile.window_ac": "AC de janela",
"lookup.home_profile.evaporative": "Evaporativo",
"lookup.home_profile.none": "Nenhum",
"lookup.home_profile.tank_gas": "Tanque (gás)",
"lookup.home_profile.tank_electric": "Tanque (elétrico)",
"lookup.home_profile.tankless_gas": "Sem tanque (gás)",
"lookup.home_profile.tankless_electric": "Sem tanque (elétrico)",
"lookup.home_profile.solar": "Solar",
"lookup.home_profile.asphalt_shingle": "Telha asfáltica",
"lookup.home_profile.metal": "Metal",
"lookup.home_profile.tile": "Telha",
"lookup.home_profile.slate": "Ardósia",
"lookup.home_profile.wood_shake": "Telha de madeira",
"lookup.home_profile.flat": "Plano",
"lookup.home_profile.brick": "Tijolo",
"lookup.home_profile.vinyl_siding": "Revestimento de vinil",
"lookup.home_profile.wood_siding": "Revestimento de madeira",
"lookup.home_profile.stucco": "Estuque",
"lookup.home_profile.stone": "Pedra",
"lookup.home_profile.fiber_cement": "Cimento reforçado",
"lookup.home_profile.hardwood": "Madeira de lei",
"lookup.home_profile.laminate": "Laminado",
"lookup.home_profile.carpet": "Carpete",
"lookup.home_profile.vinyl": "Vinil",
"lookup.home_profile.concrete": "Concreto",
"lookup.home_profile.lawn": "Gramado",
"lookup.home_profile.desert": "Deserto",
"lookup.home_profile.xeriscape": "Xeropaisagismo",
"lookup.home_profile.garden": "Jardim",
"lookup.home_profile.mixed": "Misto",
"lookup.document_type.warranty": "Garantia",
"lookup.document_type.manual": "Manual do usuário",
"lookup.document_type.receipt": "Recibo/Fatura",
"lookup.document_type.inspection": "Relatório de inspeção",
"lookup.document_type.permit": "Licença",
"lookup.document_type.deed": "Escritura/Título",
"lookup.document_type.insurance": "Seguro",
"lookup.document_type.contract": "Contrato",
"lookup.document_type.photo": "Foto",
"lookup.document_type.other": "Outro",
"lookup.document_category.appliance": "Eletrodoméstico",
"lookup.document_category.hvac": "AVAC",
"lookup.document_category.plumbing": "Encanamento",
"lookup.document_category.electrical": "Elétrica",
"lookup.document_category.roofing": "Telhado",
"lookup.document_category.structural": "Estrutural",
"lookup.document_category.landscaping": "Paisagismo",
"lookup.document_category.general": "Geral",
"lookup.document_category.other": "Outro"
} }
+85 -29
View File
@@ -25,7 +25,6 @@
"error.google_signin_not_configured": "未配置 Google 登录", "error.google_signin_not_configured": "未配置 Google 登录",
"error.google_signin_failed": "Google 登录失败", "error.google_signin_failed": "Google 登录失败",
"error.invalid_google_token": "Google 身份令牌无效", "error.invalid_google_token": "Google 身份令牌无效",
"error.invalid_task_id": "任务 ID 无效", "error.invalid_task_id": "任务 ID 无效",
"error.invalid_residence_id": "房产 ID 无效", "error.invalid_residence_id": "房产 ID 无效",
"error.invalid_contractor_id": "承包商 ID 无效", "error.invalid_contractor_id": "承包商 ID 无效",
@@ -34,7 +33,6 @@
"error.invalid_user_id": "用户 ID 无效", "error.invalid_user_id": "用户 ID 无效",
"error.invalid_notification_id": "通知 ID 无效", "error.invalid_notification_id": "通知 ID 无效",
"error.invalid_device_id": "设备 ID 无效", "error.invalid_device_id": "设备 ID 无效",
"error.task_not_found": "未找到任务", "error.task_not_found": "未找到任务",
"error.residence_not_found": "未找到房产", "error.residence_not_found": "未找到房产",
"error.contractor_not_found": "未找到承包商", "error.contractor_not_found": "未找到承包商",
@@ -43,7 +41,6 @@
"error.user_not_found": "未找到用户", "error.user_not_found": "未找到用户",
"error.share_code_invalid": "分享码无效", "error.share_code_invalid": "分享码无效",
"error.share_code_expired": "分享码已过期", "error.share_code_expired": "分享码已过期",
"error.task_access_denied": "您无权访问此任务", "error.task_access_denied": "您无权访问此任务",
"error.residence_access_denied": "您无权访问此房产", "error.residence_access_denied": "您无权访问此房产",
"error.contractor_access_denied": "您无权访问此承包商", "error.contractor_access_denied": "您无权访问此承包商",
@@ -52,10 +49,8 @@
"error.cannot_remove_owner": "无法移除房产所有者", "error.cannot_remove_owner": "无法移除房产所有者",
"error.user_already_member": "用户已是此房产的成员", "error.user_already_member": "用户已是此房产的成员",
"error.properties_limit_reached": "您已达到订阅计划的房产数量上限", "error.properties_limit_reached": "您已达到订阅计划的房产数量上限",
"error.task_already_cancelled": "任务已取消", "error.task_already_cancelled": "任务已取消",
"error.task_already_archived": "任务已归档", "error.task_already_archived": "任务已归档",
"error.failed_to_parse_form": "解析多部分表单失败", "error.failed_to_parse_form": "解析多部分表单失败",
"error.task_id_required": "需要 task_id", "error.task_id_required": "需要 task_id",
"error.invalid_task_id_value": "task_id 无效", "error.invalid_task_id_value": "task_id 无效",
@@ -64,14 +59,12 @@
"error.invalid_residence_id_value": "residence_id 无效", "error.invalid_residence_id_value": "residence_id 无效",
"error.title_required": "需要标题", "error.title_required": "需要标题",
"error.failed_to_upload_file": "上传文件失败", "error.failed_to_upload_file": "上传文件失败",
"message.logged_out": "已成功退出", "message.logged_out": "已成功退出",
"message.email_verified": "邮箱验证成功", "message.email_verified": "邮箱验证成功",
"message.verification_email_sent": "验证邮件已发送", "message.verification_email_sent": "验证邮件已发送",
"message.password_reset_email_sent": "如果该邮箱存在账户,密码重置验证码已发送。", "message.password_reset_email_sent": "如果该邮箱存在账户,密码重置验证码已发送。",
"message.reset_code_verified": "验证码验证成功", "message.reset_code_verified": "验证码验证成功",
"message.password_reset_success": "密码重置成功,请使用新密码登录。", "message.password_reset_success": "密码重置成功,请使用新密码登录。",
"message.task_deleted": "任务删除成功", "message.task_deleted": "任务删除成功",
"message.task_in_progress": "任务已标记为进行中", "message.task_in_progress": "任务已标记为进行中",
"message.task_cancelled": "任务已取消", "message.task_cancelled": "任务已取消",
@@ -79,46 +72,35 @@
"message.task_archived": "任务已归档", "message.task_archived": "任务已归档",
"message.task_unarchived": "任务已取消归档", "message.task_unarchived": "任务已取消归档",
"message.completion_deleted": "完成记录删除成功", "message.completion_deleted": "完成记录删除成功",
"message.residence_deleted": "房产删除成功", "message.residence_deleted": "房产删除成功",
"message.user_removed": "用户已从房产中移除", "message.user_removed": "用户已从房产中移除",
"message.tasks_report_generated": "任务报告生成成功", "message.tasks_report_generated": "任务报告生成成功",
"message.tasks_report_sent": "任务报告已生成并发送至 {{.Email}}", "message.tasks_report_sent": "任务报告已生成并发送至 {{.Email}}",
"message.tasks_report_email_failed": "任务报告已生成但无法发送邮件", "message.tasks_report_email_failed": "任务报告已生成但无法发送邮件",
"message.contractor_deleted": "承包商删除成功", "message.contractor_deleted": "承包商删除成功",
"message.document_deleted": "文档删除成功", "message.document_deleted": "文档删除成功",
"message.document_activated": "文档已激活", "message.document_activated": "文档已激活",
"message.document_deactivated": "文档已停用", "message.document_deactivated": "文档已停用",
"message.notification_marked_read": "通知已标记为已读", "message.notification_marked_read": "通知已标记为已读",
"message.all_notifications_marked_read": "所有通知已标记为已读", "message.all_notifications_marked_read": "所有通知已标记为已读",
"message.device_removed": "设备已移除", "message.device_removed": "设备已移除",
"message.subscription_upgraded": "订阅升级成功", "message.subscription_upgraded": "订阅升级成功",
"message.subscription_cancelled": "订阅已取消。您将保留专业版权益至当前账单周期结束。", "message.subscription_cancelled": "订阅已取消。您将保留专业版权益至当前账单周期结束。",
"message.subscription_restored": "订阅恢复成功", "message.subscription_restored": "订阅恢复成功",
"message.file_deleted": "文件删除成功", "message.file_deleted": "文件删除成功",
"message.static_data_refreshed": "静态数据已刷新", "message.static_data_refreshed": "静态数据已刷新",
"error.notification_not_found": "未找到通知", "error.notification_not_found": "未找到通知",
"error.invalid_platform": "平台无效", "error.invalid_platform": "平台无效",
"error.upgrade_trigger_not_found": "未找到升级触发器", "error.upgrade_trigger_not_found": "未找到升级触发器",
"error.receipt_data_required": "iOS 需要 receipt_data", "error.receipt_data_required": "iOS 需要 receipt_data",
"error.purchase_token_required": "Android 需要 purchase_token", "error.purchase_token_required": "Android 需要 purchase_token",
"error.no_file_provided": "未提供文件", "error.no_file_provided": "未提供文件",
"error.failed_to_fetch_residence_types": "获取房产类型失败", "error.failed_to_fetch_residence_types": "获取房产类型失败",
"error.failed_to_fetch_task_categories": "获取任务分类失败", "error.failed_to_fetch_task_categories": "获取任务分类失败",
"error.failed_to_fetch_task_priorities": "获取任务优先级失败", "error.failed_to_fetch_task_priorities": "获取任务优先级失败",
"error.failed_to_fetch_task_frequencies": "获取任务频率失败", "error.failed_to_fetch_task_frequencies": "获取任务频率失败",
"error.failed_to_fetch_task_statuses": "获取任务状态失败", "error.failed_to_fetch_task_statuses": "获取任务状态失败",
"error.failed_to_fetch_contractor_specialties": "获取承包商专业类别失败", "error.failed_to_fetch_contractor_specialties": "获取承包商专业类别失败",
"push.task_due_soon.title": "任务即将到期", "push.task_due_soon.title": "任务即将到期",
"push.task_due_soon.body": "{{.TaskTitle}} 将于 {{.DueDate}} 到期", "push.task_due_soon.body": "{{.TaskTitle}} 将于 {{.DueDate}} 到期",
"push.task_overdue.title": "任务已逾期", "push.task_overdue.title": "任务已逾期",
@@ -129,19 +111,16 @@
"push.task_assigned.body": "您已被分配到 {{.TaskTitle}}", "push.task_assigned.body": "您已被分配到 {{.TaskTitle}}",
"push.residence_shared.title": "房产已分享", "push.residence_shared.title": "房产已分享",
"push.residence_shared.body": "{{.UserName}} 与您分享了 {{.ResidenceName}}", "push.residence_shared.body": "{{.UserName}} 与您分享了 {{.ResidenceName}}",
"email.welcome.subject": "欢迎使用 honeyDue", "email.welcome.subject": "欢迎使用 honeyDue",
"email.verification.subject": "验证您的邮箱", "email.verification.subject": "验证您的邮箱",
"email.password_reset.subject": "密码重置验证码", "email.password_reset.subject": "密码重置验证码",
"email.tasks_report.subject": "{{.ResidenceName}} 的任务报告", "email.tasks_report.subject": "{{.ResidenceName}} 的任务报告",
"lookup.residence_type.house": "独栋房屋",
"lookup.residence_type.house": "独立屋",
"lookup.residence_type.apartment": "公寓", "lookup.residence_type.apartment": "公寓",
"lookup.residence_type.condo": "共管公寓", "lookup.residence_type.condo": "共管公寓",
"lookup.residence_type.townhouse": "联排别墅", "lookup.residence_type.townhouse": "联排别墅",
"lookup.residence_type.mobile_home": "移动房屋", "lookup.residence_type.mobile_home": "移动房屋",
"lookup.residence_type.other": "其他", "lookup.residence_type.other": "其他",
"lookup.task_category.plumbing": "管道", "lookup.task_category.plumbing": "管道",
"lookup.task_category.electrical": "电气", "lookup.task_category.electrical": "电气",
"lookup.task_category.hvac": "暖通空调", "lookup.task_category.hvac": "暖通空调",
@@ -154,18 +133,15 @@
"lookup.task_category.pest_control": "害虫防治", "lookup.task_category.pest_control": "害虫防治",
"lookup.task_category.seasonal": "季节性", "lookup.task_category.seasonal": "季节性",
"lookup.task_category.other": "其他", "lookup.task_category.other": "其他",
"lookup.task_priority.low": "低", "lookup.task_priority.low": "低",
"lookup.task_priority.medium": "中", "lookup.task_priority.medium": "中",
"lookup.task_priority.high": "高", "lookup.task_priority.high": "高",
"lookup.task_priority.urgent": "紧急", "lookup.task_priority.urgent": "紧急",
"lookup.task_status.pending": "待处理", "lookup.task_status.pending": "待处理",
"lookup.task_status.in_progress": "进行中", "lookup.task_status.in_progress": "进行中",
"lookup.task_status.completed": "已完成", "lookup.task_status.completed": "已完成",
"lookup.task_status.cancelled": "已取消", "lookup.task_status.cancelled": "已取消",
"lookup.task_status.archived": "已归档", "lookup.task_status.archived": "已归档",
"lookup.task_frequency.once": "一次", "lookup.task_frequency.once": "一次",
"lookup.task_frequency.daily": "每天", "lookup.task_frequency.daily": "每天",
"lookup.task_frequency.weekly": "每周", "lookup.task_frequency.weekly": "每周",
@@ -174,12 +150,11 @@
"lookup.task_frequency.quarterly": "每季度", "lookup.task_frequency.quarterly": "每季度",
"lookup.task_frequency.semiannually": "每半年", "lookup.task_frequency.semiannually": "每半年",
"lookup.task_frequency.annually": "每年", "lookup.task_frequency.annually": "每年",
"lookup.contractor_specialty.plumber": "管道工",
"lookup.contractor_specialty.plumber": "水管工",
"lookup.contractor_specialty.electrician": "电工", "lookup.contractor_specialty.electrician": "电工",
"lookup.contractor_specialty.hvac_technician": "暖通空调技师", "lookup.contractor_specialty.hvac_technician": "暖通空调技师",
"lookup.contractor_specialty.handyman": "杂工", "lookup.contractor_specialty.handyman": "杂工",
"lookup.contractor_specialty.landscaper": "园林工", "lookup.contractor_specialty.landscaper": "园艺师",
"lookup.contractor_specialty.roofer": "屋顶工", "lookup.contractor_specialty.roofer": "屋顶工",
"lookup.contractor_specialty.painter": "油漆工", "lookup.contractor_specialty.painter": "油漆工",
"lookup.contractor_specialty.carpenter": "木工", "lookup.contractor_specialty.carpenter": "木工",
@@ -187,5 +162,86 @@
"lookup.contractor_specialty.cleaning": "清洁", "lookup.contractor_specialty.cleaning": "清洁",
"lookup.contractor_specialty.pool_service": "泳池服务", "lookup.contractor_specialty.pool_service": "泳池服务",
"lookup.contractor_specialty.general_contractor": "总承包商", "lookup.contractor_specialty.general_contractor": "总承包商",
"lookup.contractor_specialty.other": "其他" "lookup.contractor_specialty.other": "其他",
"suggestion.reason.has_pool": "您的住宅有游泳池",
"suggestion.reason.has_sprinkler_system": "您的住宅有喷灌系统",
"suggestion.reason.has_septic": "您的住宅有化粪池",
"suggestion.reason.has_fireplace": "您的住宅有壁炉",
"suggestion.reason.has_garage": "您的住宅有车库",
"suggestion.reason.has_basement": "您的住宅有地下室",
"suggestion.reason.has_attic": "您的住宅有阁楼",
"suggestion.reason.heating_type": "与您的供暖系统匹配",
"suggestion.reason.cooling_type": "与您的制冷系统匹配",
"suggestion.reason.water_heater_type": "与您的热水器匹配",
"suggestion.reason.roof_type": "与您的屋顶匹配",
"suggestion.reason.exterior_type": "与您的外墙匹配",
"suggestion.reason.flooring_primary": "与您的地板匹配",
"suggestion.reason.landscaping_type": "与您的庭院匹配",
"suggestion.reason.property_type": "为您的房产类型推荐",
"suggestion.reason.climate_region": "为您所在气候推荐",
"lookup.residence_type.duplex": "双拼住宅",
"lookup.residence_type.vacation_home": "度假屋",
"lookup.task_category.general": "通用",
"lookup.task_frequency.bi_weekly": "每两周",
"lookup.task_frequency.semi_annually": "每半年",
"lookup.task_frequency.custom": "自定义",
"lookup.contractor_specialty.appliance_repair": "家电维修",
"lookup.contractor_specialty.cleaner": "清洁工",
"lookup.contractor_specialty.locksmith": "锁匠",
"lookup.home_profile.gas_furnace": "燃气炉",
"lookup.home_profile.electric_furnace": "电炉",
"lookup.home_profile.heat_pump": "热泵",
"lookup.home_profile.boiler": "锅炉",
"lookup.home_profile.radiant": "辐射式",
"lookup.home_profile.other": "其他",
"lookup.home_profile.central_ac": "中央空调",
"lookup.home_profile.window_ac": "窗式空调",
"lookup.home_profile.evaporative": "蒸发式",
"lookup.home_profile.none": "无",
"lookup.home_profile.tank_gas": "储水式(燃气)",
"lookup.home_profile.tank_electric": "储水式(电)",
"lookup.home_profile.tankless_gas": "即热式(燃气)",
"lookup.home_profile.tankless_electric": "即热式(电)",
"lookup.home_profile.solar": "太阳能",
"lookup.home_profile.asphalt_shingle": "沥青瓦",
"lookup.home_profile.metal": "金属",
"lookup.home_profile.tile": "瓦片",
"lookup.home_profile.slate": "石板",
"lookup.home_profile.wood_shake": "木瓦",
"lookup.home_profile.flat": "平顶",
"lookup.home_profile.brick": "砖",
"lookup.home_profile.vinyl_siding": "乙烯基壁板",
"lookup.home_profile.wood_siding": "木壁板",
"lookup.home_profile.stucco": "灰泥",
"lookup.home_profile.stone": "石材",
"lookup.home_profile.fiber_cement": "纤维水泥",
"lookup.home_profile.hardwood": "硬木",
"lookup.home_profile.laminate": "复合地板",
"lookup.home_profile.carpet": "地毯",
"lookup.home_profile.vinyl": "乙烯基",
"lookup.home_profile.concrete": "混凝土",
"lookup.home_profile.lawn": "草坪",
"lookup.home_profile.desert": "沙漠",
"lookup.home_profile.xeriscape": "旱生园艺",
"lookup.home_profile.garden": "花园",
"lookup.home_profile.mixed": "混合",
"lookup.document_type.warranty": "保修",
"lookup.document_type.manual": "用户手册",
"lookup.document_type.receipt": "收据/发票",
"lookup.document_type.inspection": "检查报告",
"lookup.document_type.permit": "许可证",
"lookup.document_type.deed": "契据/产权",
"lookup.document_type.insurance": "保险",
"lookup.document_type.contract": "合同",
"lookup.document_type.photo": "照片",
"lookup.document_type.other": "其他",
"lookup.document_category.appliance": "家电",
"lookup.document_category.hvac": "暖通空调",
"lookup.document_category.plumbing": "管道",
"lookup.document_category.electrical": "电气",
"lookup.document_category.roofing": "屋顶",
"lookup.document_category.structural": "结构",
"lookup.document_category.landscaping": "园艺",
"lookup.document_category.general": "通用",
"lookup.document_category.other": "其他"
} }
+38 -22
View File
@@ -12,6 +12,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/treytartt/honeydue-api/internal/config" "github.com/treytartt/honeydue-api/internal/config"
"github.com/treytartt/honeydue-api/internal/i18n"
) )
// CacheService provides Redis caching functionality // CacheService provides Redis caching functionality
@@ -133,7 +134,6 @@ func (c *CacheService) Close() error {
return nil return nil
} }
// Static data cache helpers // Static data cache helpers
const ( const (
StaticDataKey = "static_data" StaticDataKey = "static_data"
@@ -192,8 +192,10 @@ func (c *CacheService) InvalidateAllLookups(ctx context.Context) error {
LookupSpecialtiesKey, LookupSpecialtiesKey,
LookupTaskTemplatesKey, LookupTaskTemplatesKey,
StaticDataKey, // Also invalidate the combined static data StaticDataKey, // Also invalidate the combined static data
SeededDataKey, // Invalidate unified seeded data }
SeededDataETagKey, // Invalidate seeded data ETag // Per-locale seeded-data + ETag keys.
for _, lang := range i18n.SupportedLanguages {
keys = append(keys, seededDataKey(lang), seededDataETagKey(lang))
} }
return c.Delete(ctx, keys...) return c.Delete(ctx, keys...)
} }
@@ -289,50 +291,64 @@ func (c *CacheService) InvalidateTaskTemplates(ctx context.Context) error {
return c.Delete(ctx, LookupTaskTemplatesKey, StaticDataKey) return c.Delete(ctx, LookupTaskTemplatesKey, StaticDataKey)
} }
// Unified seeded data cache helpers // Unified seeded data cache helpers.
//
// The seeded-data payload is localized (lookup display_name + home-profile
// option labels), so the cache and ETag are namespaced per locale. Mixing
// locales under one key would let the first request poison every other
// language and make the ETag meaningless across locales.
const ( const (
SeededDataKey = "seeded_data" seededDataPrefix = "seeded_data:"
SeededDataETagKey = "seeded_data:etag"
SeededDataTTL = 24 * time.Hour SeededDataTTL = 24 * time.Hour
) )
// CacheSeededData caches the unified seeded data and generates an ETag func seededDataKey(locale string) string { return seededDataPrefix + locale }
func (c *CacheService) CacheSeededData(ctx context.Context, data interface{}) (string, error) { func seededDataETagKey(locale string) string { return seededDataPrefix + locale + ":etag" }
// CacheSeededData caches the unified seeded data for a locale and generates an
// ETag. The locale is folded into the ETag so a client switching languages
// always re-fetches rather than getting a stale 304.
func (c *CacheService) CacheSeededData(ctx context.Context, locale string, data interface{}) (string, error) {
jsonData, err := json.Marshal(data) jsonData, err := json.Marshal(data)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to marshal seeded data: %w", err) return "", fmt.Errorf("failed to marshal seeded data: %w", err)
} }
// Generate FNV-64a ETag from the JSON data (faster than MD5, non-cryptographic) // FNV-64a ETag over locale + JSON (faster than MD5, non-cryptographic).
h := fnv.New64a() h := fnv.New64a()
h.Write([]byte(locale))
h.Write([]byte{0})
h.Write(jsonData) h.Write(jsonData)
etag := fmt.Sprintf("\"%x\"", h.Sum64()) etag := fmt.Sprintf("\"%x\"", h.Sum64())
// Store both the data and the ETag if err := c.client.Set(ctx, seededDataKey(locale), jsonData, SeededDataTTL).Err(); err != nil {
if err := c.client.Set(ctx, SeededDataKey, jsonData, SeededDataTTL).Err(); err != nil {
return "", fmt.Errorf("failed to cache seeded data: %w", err) return "", fmt.Errorf("failed to cache seeded data: %w", err)
} }
if err := c.client.Set(ctx, seededDataETagKey(locale), etag, SeededDataTTL).Err(); err != nil {
if err := c.client.Set(ctx, SeededDataETagKey, etag, SeededDataTTL).Err(); err != nil {
return "", fmt.Errorf("failed to cache seeded data etag: %w", err) return "", fmt.Errorf("failed to cache seeded data etag: %w", err)
} }
return etag, nil return etag, nil
} }
// GetCachedSeededData retrieves cached unified seeded data // GetCachedSeededData retrieves cached unified seeded data for a locale.
func (c *CacheService) GetCachedSeededData(ctx context.Context, dest interface{}) error { func (c *CacheService) GetCachedSeededData(ctx context.Context, locale string, dest interface{}) error {
return c.Get(ctx, SeededDataKey, dest) return c.Get(ctx, seededDataKey(locale), dest)
} }
// GetSeededDataETag retrieves the cached ETag for seeded data // GetSeededDataETag retrieves the cached ETag for a locale's seeded data.
func (c *CacheService) GetSeededDataETag(ctx context.Context) (string, error) { func (c *CacheService) GetSeededDataETag(ctx context.Context, locale string) (string, error) {
return c.GetString(ctx, SeededDataETagKey) return c.GetString(ctx, seededDataETagKey(locale))
} }
// InvalidateSeededData removes cached seeded data and its ETag // InvalidateSeededData removes cached seeded data and ETags for every
// supported locale (lookup data is locale-independent at the source, so a
// change must clear all language variants).
func (c *CacheService) InvalidateSeededData(ctx context.Context) error { func (c *CacheService) InvalidateSeededData(ctx context.Context) error {
return c.Delete(ctx, SeededDataKey, SeededDataETagKey) keys := make([]string, 0, len(i18n.SupportedLanguages)*2)
for _, lang := range i18n.SupportedLanguages {
keys = append(keys, seededDataKey(lang), seededDataETagKey(lang))
}
return c.Delete(ctx, keys...)
} }
// === User → Residence-IDs cache === // === User → Residence-IDs cache ===
+159
View File
@@ -0,0 +1,159 @@
package services
import (
"strings"
goi18n "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/treytartt/honeydue-api/internal/dto/responses"
"github.com/treytartt/honeydue-api/internal/i18n"
)
// lookup kinds — the message-key namespace for each localizable lookup type.
const (
lookupKindResidenceType = "residence_type"
lookupKindTaskCategory = "task_category"
lookupKindTaskPriority = "task_priority"
lookupKindTaskFrequency = "task_frequency"
lookupKindSpecialty = "contractor_specialty"
lookupKindHomeProfile = "home_profile"
lookupKindDocumentType = "document_type"
lookupKindDocumentCat = "document_category"
)
// documentTypeValues / documentCategoryValues are the stable client enum codes
// (see iOS DocumentType/DocumentCategory). Display labels are localized at
// request time. Order is presentation order.
var documentTypeValues = []string{
"warranty", "manual", "receipt", "inspection", "permit", "deed", "insurance", "contract", "photo", "other",
}
var documentCategoryValues = []string{
"appliance", "hvac", "plumbing", "electrical", "roofing", "structural", "landscaping", "general", "other",
}
// localizedList maps a list of stable values to {value, localized display}.
func localizedList(localizer *goi18n.Localizer, kind string, values []string) []HomeProfileOption {
out := make([]HomeProfileOption, 0, len(values))
for _, v := range values {
out = append(out, HomeProfileOption{
Value: v,
DisplayName: localizeLookup(localizer, kind, v),
})
}
return out
}
// BuildDocumentTypes returns localized document-type options.
func BuildDocumentTypes(localizer *goi18n.Localizer) []HomeProfileOption {
return localizedList(localizer, lookupKindDocumentType, documentTypeValues)
}
// BuildDocumentCategories returns localized document-category options.
func BuildDocumentCategories(localizer *goi18n.Localizer) []HomeProfileOption {
return localizedList(localizer, lookupKindDocumentCat, documentCategoryValues)
}
// lookupSlug normalizes a stable English lookup name (or option value) into a
// message-key slug: lowercased, non-alphanumeric runs collapsed to "_".
// "Pest Control" -> "pest_control", "Bi-Weekly" -> "bi_weekly", "tank_gas" ->
// "tank_gas".
func lookupSlug(name string) string {
var b strings.Builder
prevUnderscore := false
for _, r := range strings.ToLower(strings.TrimSpace(name)) {
switch {
case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
b.WriteRune(r)
prevUnderscore = false
default:
if !prevUnderscore && b.Len() > 0 {
b.WriteByte('_')
prevUnderscore = true
}
}
}
return strings.Trim(b.String(), "_")
}
// localizeLookup returns the localized display label for a lookup value.
// Keys follow "lookup.<kind>.<slug>". If the locale lacks the key it falls back
// to English, and ultimately to the original name so a raw key never surfaces.
func localizeLookup(localizer *goi18n.Localizer, kind, name string) string {
key := "lookup." + kind + "." + lookupSlug(name)
msg := i18n.T(localizer, key, nil)
if msg == key {
msg = i18n.T(i18n.NewLocalizer(i18n.DefaultLanguage), key, nil)
}
if msg == key {
// No translation anywhere — fall back to the stable English name.
return name
}
return msg
}
// HomeProfileOption is a single selectable value for a home-profile field.
type HomeProfileOption struct {
Value string `json:"value"`
DisplayName string `json:"display_name"`
}
// homeProfileCatalog is the canonical set of home-profile field options,
// mirroring the (previously hardcoded) iOS dropdowns. Order is presentation
// order. Display labels are localized at request time via localizeLookup.
var homeProfileCatalog = []struct {
Field string
Values []string
}{
{"heating_type", []string{"gas_furnace", "electric_furnace", "heat_pump", "boiler", "radiant", "other"}},
{"cooling_type", []string{"central_ac", "window_ac", "heat_pump", "evaporative", "none", "other"}},
{"water_heater_type", []string{"tank_gas", "tank_electric", "tankless_gas", "tankless_electric", "heat_pump", "solar", "other"}},
{"roof_type", []string{"asphalt_shingle", "metal", "tile", "slate", "wood_shake", "flat", "other"}},
{"exterior_type", []string{"brick", "vinyl_siding", "wood_siding", "stucco", "stone", "fiber_cement", "other"}},
{"flooring_primary", []string{"hardwood", "laminate", "tile", "carpet", "vinyl", "concrete", "other"}},
{"landscaping_type", []string{"lawn", "desert", "xeriscape", "garden", "mixed", "none", "other"}},
}
// BuildHomeProfileOptions returns the home-profile field options with display
// labels localized for the supplied localizer (nil falls back to English).
func BuildHomeProfileOptions(localizer *goi18n.Localizer) map[string][]HomeProfileOption {
out := make(map[string][]HomeProfileOption, len(homeProfileCatalog))
for _, f := range homeProfileCatalog {
opts := make([]HomeProfileOption, 0, len(f.Values))
for _, v := range f.Values {
opts = append(opts, HomeProfileOption{
Value: v,
DisplayName: localizeLookup(localizer, lookupKindHomeProfile, v),
})
}
out[f.Field] = opts
}
return out
}
// LocalizeLookups fills the DisplayName of each lookup slice in place, using the
// supplied localizer. Mutates the passed slices.
func LocalizeLookups(
localizer *goi18n.Localizer,
residenceTypes []responses.ResidenceTypeResponse,
categories []responses.TaskCategoryResponse,
priorities []responses.TaskPriorityResponse,
frequencies []responses.TaskFrequencyResponse,
specialties []responses.ContractorSpecialtyResponse,
) {
for i := range residenceTypes {
residenceTypes[i].DisplayName = localizeLookup(localizer, lookupKindResidenceType, residenceTypes[i].Name)
}
for i := range categories {
categories[i].DisplayName = localizeLookup(localizer, lookupKindTaskCategory, categories[i].Name)
}
for i := range priorities {
priorities[i].DisplayName = localizeLookup(localizer, lookupKindTaskPriority, priorities[i].Name)
}
for i := range frequencies {
frequencies[i].DisplayName = localizeLookup(localizer, lookupKindTaskFrequency, frequencies[i].Name)
}
for i := range specialties {
specialties[i].DisplayName = localizeLookup(localizer, lookupKindSpecialty, specialties[i].Name)
}
}
+88
View File
@@ -0,0 +1,88 @@
package services
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/treytartt/honeydue-api/internal/dto/responses"
"github.com/treytartt/honeydue-api/internal/i18n"
)
func init() {
// Load the embedded translation bundle for the localization tests.
_ = i18n.Init()
}
func TestLookupSlug(t *testing.T) {
cases := map[string]string{
"Pest Control": "pest_control",
"Bi-Weekly": "bi_weekly",
"Semi-Annually": "semi_annually",
"Mobile Home": "mobile_home",
"HVAC": "hvac",
"tank_gas": "tank_gas",
"Tankless (Gas)": "tankless_gas",
" Trailing/Punct ": "trailing_punct",
}
for in, want := range cases {
assert.Equalf(t, want, lookupSlug(in), "slug(%q)", in)
}
}
func TestLocalizeLookup_EnglishAndSpanish(t *testing.T) {
en := i18n.NewLocalizer("en")
es := i18n.NewLocalizer("es")
// Known value: localizes per locale.
assert.Equal(t, "Plumbing", localizeLookup(en, lookupKindTaskCategory, "Plumbing"))
assert.Equal(t, "Fontanería", localizeLookup(es, lookupKindTaskCategory, "Plumbing"))
// Frequency with separators (regression on the bi_weekly/semi_annually slug).
assert.Equal(t, "Bi-Weekly", localizeLookup(en, lookupKindTaskFrequency, "Bi-Weekly"))
assert.Equal(t, "Quincenal", localizeLookup(es, lookupKindTaskFrequency, "Bi-Weekly"))
}
func TestLocalizeLookup_NilLocalizerFallsBackToEnglish(t *testing.T) {
assert.Equal(t, "House", localizeLookup(nil, lookupKindResidenceType, "House"))
}
func TestLocalizeLookup_UnknownValueFallsBackToName(t *testing.T) {
// No translation key exists -> return the stable English name, never a key.
got := localizeLookup(i18n.NewLocalizer("es"), lookupKindTaskCategory, "Totally Unknown Value")
assert.Equal(t, "Totally Unknown Value", got)
}
func TestBuildHomeProfileOptions(t *testing.T) {
opts := BuildHomeProfileOptions(i18n.NewLocalizer("es"))
// All 7 fields present.
for _, field := range []string{"heating_type", "cooling_type", "water_heater_type", "roof_type", "exterior_type", "flooring_primary", "landscaping_type"} {
require.Containsf(t, opts, field, "missing field %s", field)
require.NotEmpty(t, opts[field])
}
// Values are stable; display_name is localized.
heating := opts["heating_type"]
assert.Equal(t, "gas_furnace", heating[0].Value)
assert.Equal(t, "Calefactor de gas", heating[0].DisplayName)
}
func TestLocalizeLookups_FillsDisplayName(t *testing.T) {
cats := []responses.TaskCategoryResponse{{Name: "Plumbing"}, {Name: "HVAC"}}
prios := []responses.TaskPriorityResponse{{Name: "High"}}
freqs := []responses.TaskFrequencyResponse{{Name: "Monthly"}}
specs := []responses.ContractorSpecialtyResponse{{Name: "Plumber"}}
types := []responses.ResidenceTypeResponse{{Name: "House"}}
LocalizeLookups(i18n.NewLocalizer("es"), types, cats, prios, freqs, specs)
assert.Equal(t, "Fontanería", cats[0].DisplayName)
assert.Equal(t, "Plumbing", cats[0].Name) // name stays stable
assert.Equal(t, "Climatización", cats[1].DisplayName)
assert.Equal(t, "Alta", prios[0].DisplayName)
assert.Equal(t, "Mensual", freqs[0].DisplayName)
assert.Equal(t, "Fontanero", specs[0].DisplayName)
assert.Equal(t, "Casa", types[0].DisplayName)
}
+125 -63
View File
@@ -3,11 +3,14 @@ package services
import ( import (
"encoding/json" "encoding/json"
"sort" "sort"
"strings"
goi18n "github.com/nicksnyder/go-i18n/v2/i18n"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/treytartt/honeydue-api/internal/apperrors" "github.com/treytartt/honeydue-api/internal/apperrors"
"github.com/treytartt/honeydue-api/internal/dto/responses" "github.com/treytartt/honeydue-api/internal/dto/responses"
"github.com/treytartt/honeydue-api/internal/i18n"
"github.com/treytartt/honeydue-api/internal/models" "github.com/treytartt/honeydue-api/internal/models"
"github.com/treytartt/honeydue-api/internal/repositories" "github.com/treytartt/honeydue-api/internal/repositories"
) )
@@ -26,17 +29,51 @@ func NewSuggestionService(db *gorm.DB, residenceRepo *repositories.ResidenceRepo
} }
} }
// stringList is a condition value that may be encoded as either a single JSON
// string ("gas_furnace") or an array of allowed strings (["gas_furnace",
// "boiler"]). The seeded template catalog uses arrays of allowed values; older
// hand-written conditions used a scalar. Accepting both keeps every existing
// condition working — and a scalar `*string` (the previous type) silently
// failed to unmarshal the array form, collapsing every conditioned template to
// "universal".
type stringList []string
// UnmarshalJSON accepts a string or an array of strings.
func (s *stringList) UnmarshalJSON(data []byte) error {
var arr []string
if err := json.Unmarshal(data, &arr); err == nil {
*s = arr
return nil
}
var one string
if err := json.Unmarshal(data, &one); err != nil {
return err
}
*s = stringList{one}
return nil
}
// contains reports whether v is one of the allowed values.
func (s stringList) contains(v string) bool {
for _, x := range s {
if x == v {
return true
}
}
return false
}
// templateConditions represents the parsed conditions JSON from a task template. // templateConditions represents the parsed conditions JSON from a task template.
// Every field is optional; a template with no conditions is "universal" and // Every field is optional; a template with no conditions is "universal" and
// receives a small base score. See scoreTemplate for how each field is used. // receives a small base score. See scoreTemplate for how each field is used.
type templateConditions struct { type templateConditions struct {
HeatingType *string `json:"heating_type,omitempty"` HeatingType stringList `json:"heating_type,omitempty"`
CoolingType *string `json:"cooling_type,omitempty"` CoolingType stringList `json:"cooling_type,omitempty"`
WaterHeaterType *string `json:"water_heater_type,omitempty"` WaterHeaterType stringList `json:"water_heater_type,omitempty"`
RoofType *string `json:"roof_type,omitempty"` RoofType stringList `json:"roof_type,omitempty"`
ExteriorType *string `json:"exterior_type,omitempty"` ExteriorType stringList `json:"exterior_type,omitempty"`
FlooringPrimary *string `json:"flooring_primary,omitempty"` FlooringPrimary stringList `json:"flooring_primary,omitempty"`
LandscapingType *string `json:"landscaping_type,omitempty"` LandscapingType stringList `json:"landscaping_type,omitempty"`
HasPool *bool `json:"has_pool,omitempty"` HasPool *bool `json:"has_pool,omitempty"`
HasSprinkler *bool `json:"has_sprinkler_system,omitempty"` HasSprinkler *bool `json:"has_sprinkler_system,omitempty"`
HasSeptic *bool `json:"has_septic,omitempty"` HasSeptic *bool `json:"has_septic,omitempty"`
@@ -44,7 +81,7 @@ type templateConditions struct {
HasGarage *bool `json:"has_garage,omitempty"` HasGarage *bool `json:"has_garage,omitempty"`
HasBasement *bool `json:"has_basement,omitempty"` HasBasement *bool `json:"has_basement,omitempty"`
HasAttic *bool `json:"has_attic,omitempty"` HasAttic *bool `json:"has_attic,omitempty"`
PropertyType *string `json:"property_type,omitempty"` PropertyType stringList `json:"property_type,omitempty"`
// ClimateRegionID replaces the old task_tasktemplate_regions join table. // ClimateRegionID replaces the old task_tasktemplate_regions join table.
// Tag a template with the IECC zone ID it's relevant to (e.g. "Winterize // Tag a template with the IECC zone ID it's relevant to (e.g. "Winterize
// Sprinkler" → zone 5/6). Residence.PostalCode is mapped to a region at // Sprinkler" → zone 5/6). Residence.PostalCode is mapped to a region at
@@ -54,12 +91,12 @@ type templateConditions struct {
// isEmpty returns true if no conditions are set // isEmpty returns true if no conditions are set
func (c *templateConditions) isEmpty() bool { func (c *templateConditions) isEmpty() bool {
return c.HeatingType == nil && c.CoolingType == nil && c.WaterHeaterType == nil && return len(c.HeatingType) == 0 && len(c.CoolingType) == 0 && len(c.WaterHeaterType) == 0 &&
c.RoofType == nil && c.ExteriorType == nil && c.FlooringPrimary == nil && len(c.RoofType) == 0 && len(c.ExteriorType) == 0 && len(c.FlooringPrimary) == 0 &&
c.LandscapingType == nil && c.HasPool == nil && c.HasSprinkler == nil && len(c.LandscapingType) == 0 && c.HasPool == nil && c.HasSprinkler == nil &&
c.HasSeptic == nil && c.HasFireplace == nil && c.HasGarage == nil && c.HasSeptic == nil && c.HasFireplace == nil && c.HasGarage == nil &&
c.HasBasement == nil && c.HasAttic == nil && c.HasBasement == nil && c.HasAttic == nil &&
c.PropertyType == nil && c.ClimateRegionID == nil len(c.PropertyType) == 0 && c.ClimateRegionID == nil
} }
const ( const (
@@ -75,8 +112,10 @@ const (
totalProfileFields = 15 // 14 home-profile fields + ZIP/region totalProfileFields = 15 // 14 home-profile fields + ZIP/region
) )
// GetSuggestions returns task template suggestions scored against a residence's profile // GetSuggestions returns task template suggestions scored against a residence's
func (s *SuggestionService) GetSuggestions(residenceID uint, userID uint) (*responses.TaskSuggestionsResponse, error) { // profile. Match reasons are localized for display via the supplied localizer
// (nil falls back to English).
func (s *SuggestionService) GetSuggestions(residenceID uint, userID uint, localizer *goi18n.Localizer) (*responses.TaskSuggestionsResponse, error) {
// Check access // Check access
hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID)
if err != nil { if err != nil {
@@ -112,7 +151,7 @@ func (s *SuggestionService) GetSuggestions(residenceID uint, userID uint) (*resp
suggestions = append(suggestions, responses.TaskSuggestionResponse{ suggestions = append(suggestions, responses.TaskSuggestionResponse{
Template: responses.NewTaskTemplateResponse(&templates[i]), Template: responses.NewTaskTemplateResponse(&templates[i]),
RelevanceScore: score, RelevanceScore: score,
MatchReasons: reasons, MatchReasons: localizeReasons(localizer, reasons),
}) })
} }
@@ -135,6 +174,38 @@ func (s *SuggestionService) GetSuggestions(residenceID uint, userID uint) (*resp
}, nil }, nil
} }
// localizeReasons converts the internal reason codes emitted by scoreTemplate
// into human-readable, localized strings for the API response.
//
// Codes come in two shapes: a bare feature code ("has_fireplace") and a
// "field:value" pair ("heating_type:gas_furnace"); for the latter we key off
// the field only (the percentage already conveys strength, and a per-enum-value
// catalog would be a much larger surface). The "universal" and "partial_profile"
// signals are internal scoring artifacts, not user-facing reasons, so they're
// dropped. Any code without a translation falls back to English so a raw key
// can never leak to the UI.
func localizeReasons(localizer *goi18n.Localizer, codes []string) []string {
out := make([]string, 0, len(codes))
for _, code := range codes {
if code == "universal" || code == "partial_profile" {
continue
}
field := code
if i := strings.IndexByte(code, ':'); i >= 0 {
field = code[:i]
}
key := "suggestion.reason." + field
msg := i18n.T(localizer, key, nil)
if msg == key {
// Locale lacked the key — fall back to English so the user never
// sees a raw message id.
msg = i18n.T(i18n.NewLocalizer(i18n.DefaultLanguage), key, nil)
}
out = append(out, msg)
}
return out
}
// scoreTemplate scores a template against a residence profile. // scoreTemplate scores a template against a residence profile.
// Returns (score, matchReasons, shouldInclude). // Returns (score, matchReasons, shouldInclude).
func (s *SuggestionService) scoreTemplate(tmpl *models.TaskTemplate, residence *models.Residence) (float64, []string, bool) { func (s *SuggestionService) scoreTemplate(tmpl *models.TaskTemplate, residence *models.Residence) (float64, []string, bool) {
@@ -157,76 +228,62 @@ func (s *SuggestionService) scoreTemplate(tmpl *models.TaskTemplate, residence *
reasons := make([]string, 0) reasons := make([]string, 0)
conditionCount := 0 conditionCount := 0
// String field matches // String field matches. Each condition is a set of allowed values; the
if cond.HeatingType != nil { // residence matches when its value is one of them. A nil residence field is
// ignored (no penalty, no exclusion); a mismatch simply earns no bonus.
if len(cond.HeatingType) > 0 {
conditionCount++ conditionCount++
if residence.HeatingType == nil { if residence.HeatingType != nil && cond.HeatingType.contains(*residence.HeatingType) {
// Field not set - ignore
} else if *residence.HeatingType == *cond.HeatingType {
score += stringMatchBonus score += stringMatchBonus
reasons = append(reasons, "heating_type:"+*cond.HeatingType) reasons = append(reasons, "heating_type:"+*residence.HeatingType)
} else {
// Mismatch - don't exclude, just don't reward
} }
} }
if cond.CoolingType != nil { if len(cond.CoolingType) > 0 {
conditionCount++ conditionCount++
if residence.CoolingType == nil { if residence.CoolingType != nil && cond.CoolingType.contains(*residence.CoolingType) {
// ignore
} else if *residence.CoolingType == *cond.CoolingType {
score += stringMatchBonus score += stringMatchBonus
reasons = append(reasons, "cooling_type:"+*cond.CoolingType) reasons = append(reasons, "cooling_type:"+*residence.CoolingType)
} }
} }
if cond.WaterHeaterType != nil { if len(cond.WaterHeaterType) > 0 {
conditionCount++ conditionCount++
if residence.WaterHeaterType == nil { if residence.WaterHeaterType != nil && cond.WaterHeaterType.contains(*residence.WaterHeaterType) {
// ignore
} else if *residence.WaterHeaterType == *cond.WaterHeaterType {
score += stringMatchBonus score += stringMatchBonus
reasons = append(reasons, "water_heater_type:"+*cond.WaterHeaterType) reasons = append(reasons, "water_heater_type:"+*residence.WaterHeaterType)
} }
} }
if cond.RoofType != nil { if len(cond.RoofType) > 0 {
conditionCount++ conditionCount++
if residence.RoofType == nil { if residence.RoofType != nil && cond.RoofType.contains(*residence.RoofType) {
// ignore
} else if *residence.RoofType == *cond.RoofType {
score += stringMatchBonus score += stringMatchBonus
reasons = append(reasons, "roof_type:"+*cond.RoofType) reasons = append(reasons, "roof_type:"+*residence.RoofType)
} }
} }
if cond.ExteriorType != nil { if len(cond.ExteriorType) > 0 {
conditionCount++ conditionCount++
if residence.ExteriorType == nil { if residence.ExteriorType != nil && cond.ExteriorType.contains(*residence.ExteriorType) {
// ignore
} else if *residence.ExteriorType == *cond.ExteriorType {
score += stringMatchBonus score += stringMatchBonus
reasons = append(reasons, "exterior_type:"+*cond.ExteriorType) reasons = append(reasons, "exterior_type:"+*residence.ExteriorType)
} }
} }
if cond.FlooringPrimary != nil { if len(cond.FlooringPrimary) > 0 {
conditionCount++ conditionCount++
if residence.FlooringPrimary == nil { if residence.FlooringPrimary != nil && cond.FlooringPrimary.contains(*residence.FlooringPrimary) {
// ignore
} else if *residence.FlooringPrimary == *cond.FlooringPrimary {
score += stringMatchBonus score += stringMatchBonus
reasons = append(reasons, "flooring_primary:"+*cond.FlooringPrimary) reasons = append(reasons, "flooring_primary:"+*residence.FlooringPrimary)
} }
} }
if cond.LandscapingType != nil { if len(cond.LandscapingType) > 0 {
conditionCount++ conditionCount++
if residence.LandscapingType == nil { if residence.LandscapingType != nil && cond.LandscapingType.contains(*residence.LandscapingType) {
// ignore
} else if *residence.LandscapingType == *cond.LandscapingType {
score += stringMatchBonus score += stringMatchBonus
reasons = append(reasons, "landscaping_type:"+*cond.LandscapingType) reasons = append(reasons, "landscaping_type:"+*residence.LandscapingType)
} }
} }
@@ -309,11 +366,11 @@ func (s *SuggestionService) scoreTemplate(tmpl *models.TaskTemplate, residence *
} }
// Property type match // Property type match
if cond.PropertyType != nil { if len(cond.PropertyType) > 0 {
conditionCount++ conditionCount++
if residence.PropertyType != nil && residence.PropertyType.Name == *cond.PropertyType { if residence.PropertyType != nil && cond.PropertyType.contains(residence.PropertyType.Name) {
score += propertyTypeBonus score += propertyTypeBonus
reasons = append(reasons, "property_type:"+*cond.PropertyType) reasons = append(reasons, "property_type:"+residence.PropertyType.Name)
} }
} }
@@ -328,17 +385,22 @@ func (s *SuggestionService) scoreTemplate(tmpl *models.TaskTemplate, residence *
} }
} }
// Cap at 1.0 // If template has conditions but none matched (residence fields unset or
if score > 1.0 { // different), rank it just below universal — it's a conditioned template we
score = 1.0 // couldn't confirm applies.
}
// If template has conditions but no matches and no reasons, still include with low score
if conditionCount > 0 && len(reasons) == 0 { if conditionCount > 0 && len(reasons) == 0 {
return baseUniversalScore * 0.5, []string{"partial_profile"}, true return baseUniversalScore * 0.5, []string{"partial_profile"}, true
} }
return score, reasons, true // A matched conditioned template must rank ABOVE a universal one. Anchor it
// at the universal baseline and add the accumulated match bonuses on top,
// so e.g. a single heating match (0.3 + 0.25) clearly beats universal (0.3).
final := baseUniversalScore + score
if final > 1.0 {
final = 1.0
}
return final, reasons, true
} }
// CalculateProfileCompleteness returns how many of the 14 home profile fields are filled // CalculateProfileCompleteness returns how many of the 14 home profile fields are filled
+91 -56
View File
@@ -47,12 +47,12 @@ func TestSuggestionService_UniversalTemplate(t *testing.T) {
// Create universal template (empty conditions) // Create universal template (empty conditions)
createTemplateWithConditions(t, service, "Change Air Filters", nil) createTemplateWithConditions(t, service, "Change Air Filters", nil)
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Equal(t, "Change Air Filters", resp.Suggestions[0].Template.Title) assert.Equal(t, "Change Air Filters", resp.Suggestions[0].Template.Title)
assert.Equal(t, baseUniversalScore, resp.Suggestions[0].RelevanceScore) assert.Equal(t, baseUniversalScore, resp.Suggestions[0].RelevanceScore)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "universal") assert.Empty(t, resp.Suggestions[0].MatchReasons) // universal templates carry no display reason
} }
func TestSuggestionService_HeatingTypeMatch(t *testing.T) { func TestSuggestionService_HeatingTypeMatch(t *testing.T) {
@@ -75,11 +75,47 @@ func TestSuggestionService_HeatingTypeMatch(t *testing.T) {
"heating_type": "gas_furnace", "heating_type": "gas_furnace",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Equal(t, stringMatchBonus, resp.Suggestions[0].RelevanceScore) // Matched conditioned templates are anchored at the universal baseline and
assert.Contains(t, resp.Suggestions[0].MatchReasons, "heating_type:gas_furnace") // earn bonuses on top, so a match always ranks above a universal template.
assert.Equal(t, baseUniversalScore+stringMatchBonus, resp.Suggestions[0].RelevanceScore)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "Matches your heating system")
}
// TestSuggestionService_StringConditionArrayForm is the regression test for the
// scorer bug where conditions encoded as an array of allowed values
// (`{"heating_type":["gas_furnace","boiler"]}`, the format used by the seeded
// template catalog) failed to unmarshal into a scalar *string field and the
// template silently collapsed to "universal". The residence's value must match
// when it is any member of the array.
func TestSuggestionService_StringConditionArrayForm(t *testing.T) {
service := setupSuggestionService(t)
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "password")
heatingType := "boiler" // second value in the allowed list
residence := &models.Residence{
OwnerID: user.ID,
Name: "Boiler House",
IsActive: true,
IsPrimary: true,
HeatingType: &heatingType,
}
require.NoError(t, service.db.Create(residence).Error)
// Array-of-allowed-values form, exactly as the seed catalog stores it.
createTemplateWithConditions(t, service, "Test Gas Shutoffs", map[string]interface{}{
"heating_type": []string{"gas_furnace", "boiler"},
})
resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err)
require.Len(t, resp.Suggestions, 1)
// Must MATCH (not fall back to universal) and rank above the universal baseline.
assert.Equal(t, baseUniversalScore+stringMatchBonus, resp.Suggestions[0].RelevanceScore)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "Matches your heating system")
assert.Len(t, resp.Suggestions[0].MatchReasons, 1)
} }
func TestSuggestionService_ExcludedWhenPoolRequiredButFalse(t *testing.T) { func TestSuggestionService_ExcludedWhenPoolRequiredButFalse(t *testing.T) {
@@ -94,7 +130,7 @@ func TestSuggestionService_ExcludedWhenPoolRequiredButFalse(t *testing.T) {
"has_pool": true, "has_pool": true,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, resp.Suggestions, 0) // Should be excluded assert.Len(t, resp.Suggestions, 0) // Should be excluded
} }
@@ -111,11 +147,11 @@ func TestSuggestionService_NilFieldIgnored(t *testing.T) {
"heating_type": "gas_furnace", "heating_type": "gas_furnace",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
// Should be included (not excluded) but with low partial score // Should be included (not excluded) but with low partial score
assert.Contains(t, resp.Suggestions[0].MatchReasons, "partial_profile") assert.Empty(t, resp.Suggestions[0].MatchReasons) // conditioned-but-unmatched carries no display reason
} }
func TestSuggestionService_ProfileCompleteness(t *testing.T) { func TestSuggestionService_ProfileCompleteness(t *testing.T) {
@@ -140,7 +176,7 @@ func TestSuggestionService_ProfileCompleteness(t *testing.T) {
// Create at least one template so we get a response // Create at least one template so we get a response
createTemplateWithConditions(t, service, "Universal Task", nil) createTemplateWithConditions(t, service, "Universal Task", nil)
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
// 4 fields filled out of 15 (home-profile fields + ZIP/region) // 4 fields filled out of 15 (home-profile fields + ZIP/region)
expectedCompleteness := 4.0 / float64(totalProfileFields) expectedCompleteness := 4.0 / float64(totalProfileFields)
@@ -176,7 +212,7 @@ func TestSuggestionService_SortedByScoreDescending(t *testing.T) {
"has_pool": true, "has_pool": true,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 3) require.Len(t, resp.Suggestions, 3)
@@ -193,7 +229,7 @@ func TestSuggestionService_AccessDenied(t *testing.T) {
residence := testutil.CreateTestResidence(t, service.db, owner.ID, "Private House") residence := testutil.CreateTestResidence(t, service.db, owner.ID, "Private House")
_, err := service.GetSuggestions(residence.ID, stranger.ID) _, err := service.GetSuggestions(residence.ID, stranger.ID, nil)
require.Error(t, err) require.Error(t, err)
testutil.AssertAppErrorCode(t, err, 403) testutil.AssertAppErrorCode(t, err, 403)
} }
@@ -222,12 +258,13 @@ func TestSuggestionService_MultipleConditionsAllMustMatch(t *testing.T) {
"heating_type": "gas_furnace", "heating_type": "gas_furnace",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
// All three conditions matched // All three conditions matched. Anchored baseline (0.3) + 0.3 + 0.3 + 0.25
expectedScore := boolMatchBonus + boolMatchBonus + stringMatchBonus // 0.3 + 0.3 + 0.25 = 0.85 // = 1.15, capped at 1.0.
expectedScore := 1.0
assert.InDelta(t, expectedScore, resp.Suggestions[0].RelevanceScore, 0.01) assert.InDelta(t, expectedScore, resp.Suggestions[0].RelevanceScore, 0.01)
assert.Len(t, resp.Suggestions[0].MatchReasons, 3) assert.Len(t, resp.Suggestions[0].MatchReasons, 3)
} }
@@ -248,7 +285,7 @@ func TestSuggestionService_MalformedConditions(t *testing.T) {
err := service.db.Create(tmpl).Error err := service.db.Create(tmpl).Error
require.NoError(t, err) require.NoError(t, err)
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
// Should be treated as universal // Should be treated as universal
@@ -270,7 +307,7 @@ func TestSuggestionService_NullConditions(t *testing.T) {
err := service.db.Create(tmpl).Error err := service.db.Create(tmpl).Error
require.NoError(t, err) require.NoError(t, err)
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Equal(t, baseUniversalScore, resp.Suggestions[0].RelevanceScore) assert.Equal(t, baseUniversalScore, resp.Suggestions[0].RelevanceScore)
@@ -304,10 +341,10 @@ func TestSuggestionService_PropertyTypeMatch(t *testing.T) {
"property_type": "House", "property_type": "House",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "property_type:House") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Recommended for your property type")
} }
// === CalculateProfileCompleteness with fully filled profile === // === CalculateProfileCompleteness with fully filled profile ===
@@ -392,7 +429,7 @@ func TestSuggestionService_ScoreCappedAtOne(t *testing.T) {
"has_garage": true, "has_garage": true,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.LessOrEqual(t, resp.Suggestions[0].RelevanceScore, 1.0) assert.LessOrEqual(t, resp.Suggestions[0].RelevanceScore, 1.0)
@@ -409,7 +446,7 @@ func TestSuggestionService_InactiveTemplateExcluded(t *testing.T) {
err := service.db.Exec("INSERT INTO task_tasktemplate (title, is_active, conditions, created_at, updated_at) VALUES ('Inactive Task', false, '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)").Error err := service.db.Exec("INSERT INTO task_tasktemplate (title, is_active, conditions, created_at, updated_at) VALUES ('Inactive Task', false, '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)").Error
require.NoError(t, err) require.NoError(t, err)
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, resp.Suggestions, 0) assert.Len(t, resp.Suggestions, 0)
} }
@@ -425,7 +462,7 @@ func TestSuggestionService_ExcludedWhenSprinklerRequired(t *testing.T) {
"has_sprinkler_system": true, "has_sprinkler_system": true,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, resp.Suggestions, 0) assert.Len(t, resp.Suggestions, 0)
} }
@@ -441,7 +478,7 @@ func TestSuggestionService_ExcludedWhenSepticRequired(t *testing.T) {
"has_septic": true, "has_septic": true,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, resp.Suggestions, 0) assert.Len(t, resp.Suggestions, 0)
} }
@@ -455,7 +492,7 @@ func TestSuggestionService_ExcludedWhenFireplaceRequired(t *testing.T) {
"has_fireplace": true, "has_fireplace": true,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, resp.Suggestions, 0) assert.Len(t, resp.Suggestions, 0)
} }
@@ -469,7 +506,7 @@ func TestSuggestionService_ExcludedWhenGarageRequired(t *testing.T) {
"has_garage": true, "has_garage": true,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, resp.Suggestions, 0) assert.Len(t, resp.Suggestions, 0)
} }
@@ -483,7 +520,7 @@ func TestSuggestionService_ExcludedWhenBasementRequired(t *testing.T) {
"has_basement": true, "has_basement": true,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, resp.Suggestions, 0) assert.Len(t, resp.Suggestions, 0)
} }
@@ -497,7 +534,7 @@ func TestSuggestionService_ExcludedWhenAtticRequired(t *testing.T) {
"has_attic": true, "has_attic": true,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, resp.Suggestions, 0) assert.Len(t, resp.Suggestions, 0)
} }
@@ -523,10 +560,10 @@ func TestSuggestionService_CoolingTypeMatch(t *testing.T) {
"cooling_type": "central_ac", "cooling_type": "central_ac",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "cooling_type:central_ac") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Matches your cooling system")
} }
func TestSuggestionService_WaterHeaterTypeMatch(t *testing.T) { func TestSuggestionService_WaterHeaterTypeMatch(t *testing.T) {
@@ -548,10 +585,10 @@ func TestSuggestionService_WaterHeaterTypeMatch(t *testing.T) {
"water_heater_type": "tank_gas", "water_heater_type": "tank_gas",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "water_heater_type:tank_gas") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Matches your water heater")
} }
func TestSuggestionService_ExteriorTypeMatch(t *testing.T) { func TestSuggestionService_ExteriorTypeMatch(t *testing.T) {
@@ -573,10 +610,10 @@ func TestSuggestionService_ExteriorTypeMatch(t *testing.T) {
"exterior_type": "brick", "exterior_type": "brick",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "exterior_type:brick") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Matches your exterior")
} }
func TestSuggestionService_FlooringPrimaryMatch(t *testing.T) { func TestSuggestionService_FlooringPrimaryMatch(t *testing.T) {
@@ -598,10 +635,10 @@ func TestSuggestionService_FlooringPrimaryMatch(t *testing.T) {
"flooring_primary": "hardwood", "flooring_primary": "hardwood",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "flooring_primary:hardwood") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Matches your flooring")
} }
func TestSuggestionService_LandscapingTypeMatch(t *testing.T) { func TestSuggestionService_LandscapingTypeMatch(t *testing.T) {
@@ -623,10 +660,10 @@ func TestSuggestionService_LandscapingTypeMatch(t *testing.T) {
"landscaping_type": "lawn", "landscaping_type": "lawn",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "landscaping_type:lawn") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Matches your landscaping")
} }
func TestSuggestionService_RoofTypeMatch(t *testing.T) { func TestSuggestionService_RoofTypeMatch(t *testing.T) {
@@ -648,10 +685,10 @@ func TestSuggestionService_RoofTypeMatch(t *testing.T) {
"roof_type": "asphalt_shingle", "roof_type": "asphalt_shingle",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "roof_type:asphalt_shingle") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Matches your roof")
} }
// === Mismatch on string field — no score for that field === // === Mismatch on string field — no score for that field ===
@@ -676,11 +713,11 @@ func TestSuggestionService_HeatingTypeMismatch(t *testing.T) {
"heating_type": "gas_furnace", "heating_type": "gas_furnace",
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
// Should still be included but with partial_profile (no match, no exclude) // Should still be included but with partial_profile (no match, no exclude)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "partial_profile") assert.Empty(t, resp.Suggestions[0].MatchReasons) // conditioned-but-unmatched carries no display reason
} }
// === templateConditions.isEmpty === // === templateConditions.isEmpty ===
@@ -689,16 +726,14 @@ func TestTemplateConditions_IsEmpty(t *testing.T) {
cond := &templateConditions{} cond := &templateConditions{}
assert.True(t, cond.isEmpty()) assert.True(t, cond.isEmpty())
ht := "gas" cond2 := &templateConditions{HeatingType: stringList{"gas"}}
cond2 := &templateConditions{HeatingType: &ht}
assert.False(t, cond2.isEmpty()) assert.False(t, cond2.isEmpty())
pool := true pool := true
cond3 := &templateConditions{HasPool: &pool} cond3 := &templateConditions{HasPool: &pool}
assert.False(t, cond3.isEmpty()) assert.False(t, cond3.isEmpty())
pt := "House" cond4 := &templateConditions{PropertyType: stringList{"House"}}
cond4 := &templateConditions{PropertyType: &pt}
assert.False(t, cond4.isEmpty()) assert.False(t, cond4.isEmpty())
var regionID uint = 5 var regionID uint = 5
@@ -727,11 +762,11 @@ func TestSuggestionService_ClimateRegionMatch(t *testing.T) {
"climate_region_id": 5, "climate_region_id": 5,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
assert.InDelta(t, climateRegionBonus, resp.Suggestions[0].RelevanceScore, 0.001) assert.InDelta(t, baseUniversalScore+climateRegionBonus, resp.Suggestions[0].RelevanceScore, 0.001)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "climate_region") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Recommended for your climate")
} }
func TestSuggestionService_ClimateRegionMismatch(t *testing.T) { func TestSuggestionService_ClimateRegionMismatch(t *testing.T) {
@@ -753,11 +788,11 @@ func TestSuggestionService_ClimateRegionMismatch(t *testing.T) {
"climate_region_id": 6, "climate_region_id": 6,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) // Still included — mismatch doesn't exclude require.Len(t, resp.Suggestions, 1) // Still included — mismatch doesn't exclude
assert.InDelta(t, baseUniversalScore*0.5, resp.Suggestions[0].RelevanceScore, 0.001) assert.InDelta(t, baseUniversalScore*0.5, resp.Suggestions[0].RelevanceScore, 0.001)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "partial_profile") assert.Empty(t, resp.Suggestions[0].MatchReasons) // conditioned-but-unmatched carries no display reason
} }
func TestSuggestionService_ClimateRegionIgnoredWhenNoZip(t *testing.T) { func TestSuggestionService_ClimateRegionIgnoredWhenNoZip(t *testing.T) {
@@ -779,7 +814,7 @@ func TestSuggestionService_ClimateRegionIgnoredWhenNoZip(t *testing.T) {
"climate_region_id": 5, "climate_region_id": 5,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) // Still included, just no bonus require.Len(t, resp.Suggestions, 1) // Still included, just no bonus
assert.InDelta(t, baseUniversalScore*0.5, resp.Suggestions[0].RelevanceScore, 0.001) assert.InDelta(t, baseUniversalScore*0.5, resp.Suggestions[0].RelevanceScore, 0.001)
@@ -802,11 +837,11 @@ func TestSuggestionService_ClimateRegionUnknownZip(t *testing.T) {
"climate_region_id": 5, "climate_region_id": 5,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
// Unknown ZIP → 0 region → no match, but no crash // Unknown ZIP → 0 region → no match, but no crash
assert.Contains(t, resp.Suggestions[0].MatchReasons, "partial_profile") assert.Empty(t, resp.Suggestions[0].MatchReasons) // conditioned-but-unmatched carries no display reason
} }
func TestSuggestionService_ClimateRegionStacksWithOtherConditions(t *testing.T) { func TestSuggestionService_ClimateRegionStacksWithOtherConditions(t *testing.T) {
@@ -829,11 +864,11 @@ func TestSuggestionService_ClimateRegionStacksWithOtherConditions(t *testing.T)
"climate_region_id": 5, "climate_region_id": 5,
}) })
resp, err := service.GetSuggestions(residence.ID, user.ID) resp, err := service.GetSuggestions(residence.ID, user.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Suggestions, 1) require.Len(t, resp.Suggestions, 1)
// Both bonuses should apply: stringMatchBonus + climateRegionBonus // Both bonuses should apply: stringMatchBonus + climateRegionBonus
assert.InDelta(t, stringMatchBonus+climateRegionBonus, resp.Suggestions[0].RelevanceScore, 0.001) assert.InDelta(t, baseUniversalScore+stringMatchBonus+climateRegionBonus, resp.Suggestions[0].RelevanceScore, 0.001)
assert.Contains(t, resp.Suggestions[0].MatchReasons, "heating_type:gas_furnace") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Matches your heating system")
assert.Contains(t, resp.Suggestions[0].MatchReasons, "climate_region") assert.Contains(t, resp.Suggestions[0].MatchReasons, "Recommended for your climate")
} }