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
+25 -11
View File
@@ -15,12 +15,15 @@ import (
// SeededDataResponse represents the unified seeded data response
type SeededDataResponse struct {
ResidenceTypes interface{} `json:"residence_types"`
TaskCategories interface{} `json:"task_categories"`
TaskPriorities interface{} `json:"task_priorities"`
TaskFrequencies interface{} `json:"task_frequencies"`
ContractorSpecialties interface{} `json:"contractor_specialties"`
TaskTemplates responses.TaskTemplatesGroupedResponse `json:"task_templates"`
ResidenceTypes interface{} `json:"residence_types"`
TaskCategories interface{} `json:"task_categories"`
TaskPriorities interface{} `json:"task_priorities"`
TaskFrequencies interface{} `json:"task_frequencies"`
ContractorSpecialties interface{} `json:"contractor_specialties"`
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
@@ -54,13 +57,18 @@ func NewStaticDataHandler(
func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
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
// 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/")
// Try to get cached ETag first (fast path for 304 responses)
if h.cache != nil && clientETag != "" {
cachedETag, err := h.cache.GetSeededDataETag(ctx)
cachedETag, err := h.cache.GetSeededDataETag(ctx, locale)
if err == nil && cachedETag == clientETag {
// Client has the latest data, return 304 Not Modified
return c.NoContent(http.StatusNotModified)
@@ -70,10 +78,10 @@ func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
// Try to get cached seeded data
if h.cache != nil {
var cachedData SeededDataResponse
err := h.cache.GetCachedSeededData(ctx, &cachedData)
err := h.cache.GetCachedSeededData(ctx, locale, &cachedData)
if err == nil {
// Cache hit - get the ETag and return data
etag, etagErr := h.cache.GetSeededDataETag(ctx)
etag, etagErr := h.cache.GetSeededDataETag(ctx, locale)
if etagErr == nil {
c.Response().Header().Set("ETag", etag)
c.Response().Header().Set("Cache-Control", "private, max-age=3600")
@@ -116,6 +124,9 @@ func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
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
seededData := SeededDataResponse{
ResidenceTypes: residenceTypes,
@@ -124,11 +135,14 @@ func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
TaskFrequencies: taskFrequencies,
ContractorSpecialties: contractorSpecialties,
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 {
etag, cacheErr := h.cache.CacheSeededData(ctx, seededData)
etag, cacheErr := h.cache.CacheSeededData(ctx, locale, seededData)
if cacheErr != nil {
log.Warn().Err(cacheErr).Msg("Failed to cache seeded data")
} else {