package locale import ( "context" "fmt" "net/http" "strings" "github.com/jackc/pgx/v5/pgxpool" ) // Default is the fallback language when no supported language is detected. const Default = "en" // Supported is the set of language codes the application currently handles. // Keys are ISO 639-1 two-letter codes (lower-case). // Populated by LoadFromDB at server startup. var Supported = map[string]bool{ "en": true, "ru": true, } // Language is a supported language record loaded from the DB. type Language struct { Code string NativeName string EnglishName string } // Languages is the ordered list of active languages. // Populated by LoadFromDB at server startup. var Languages []Language // LoadFromDB queries the languages table and updates both Supported and Languages. // Must be called once at startup before the server begins accepting requests. func LoadFromDB(ctx context.Context, pool *pgxpool.Pool) error { rows, err := pool.Query(ctx, `SELECT code, native_name, english_name FROM languages WHERE is_active ORDER BY sort_order`) if err != nil { return fmt.Errorf("load languages from db: %w", err) } defer rows.Close() newSupported := map[string]bool{} var newLanguages []Language for rows.Next() { var l Language if err := rows.Scan(&l.Code, &l.NativeName, &l.EnglishName); err != nil { return err } newSupported[l.Code] = true newLanguages = append(newLanguages, l) } if err := rows.Err(); err != nil { return err } Supported = newSupported Languages = newLanguages return nil } type contextKey struct{} // Parse returns the best-matching supported language from an Accept-Language // header value. It iterates through the comma-separated list in preference // order and returns the first entry whose primary subtag is in Supported. // Returns Default when the header is empty or no match is found. func Parse(acceptLang string) string { if acceptLang == "" { return Default } for part := range strings.SplitSeq(acceptLang, ",") { // Strip quality value (e.g. ";q=0.9"). tag := strings.TrimSpace(strings.SplitN(part, ";", 2)[0]) // Use only the primary subtag (e.g. "ru" from "ru-RU"). lang := strings.ToLower(strings.SplitN(tag, "-", 2)[0]) if Supported[lang] { return lang } } return Default } // WithLang returns a copy of ctx carrying the given language code. func WithLang(ctx context.Context, lang string) context.Context { return context.WithValue(ctx, contextKey{}, lang) } // FromContext returns the language stored in ctx. // Returns Default when no language has been set. func FromContext(ctx context.Context) string { if lang, ok := ctx.Value(contextKey{}).(string); ok && lang != "" { return lang } return Default } // FromRequest extracts the preferred language from the request's // Accept-Language header. func FromRequest(r *http.Request) string { return Parse(r.Header.Get("Accept-Language")) }