LCOV - code coverage report
Current view: top level - adapter/http/middleware - locale.go Coverage Total Hit
Test: coverage.lcov Lines: 64.6 % 79 51
Test Date: 2026-04-14 06:42:22 Functions: - 0 0

            Line data    Source code
       1              : // Package middleware は、HTTPリクエストからロケールを交渉・取得するミドルウェアを提供します。
       2              : package middleware
       3              : 
       4              : import (
       5              :         "net/http"
       6              :         "strings"
       7              : 
       8              :         "github.com/gin-gonic/gin"
       9              : )
      10              : 
      11              : // LocaleSource は利用可能ロケールコードの列挙だけを要求する最小インターフェイスです。
      12              : // 旧実装の Store がこのメソッドを持っていれば、そのまま渡せます。
      13              : type LocaleSource interface {
      14              :         AvailableLocaleCodes() []string
      15              : }
      16              : 
      17              : // Opts はロケールネゴシエーションの設定です(旧Optsに準拠)。
      18              : type Opts struct {
      19              :         Default   string       // 既定言語(例: "ja")
      20              :         Allow     []string     // 明示指定。空なら Store から自動検出
      21              :         Query     string       // 例: "lang"(空なら無効)
      22              :         Header    string       // 例: "Accept-Language"
      23              :         Cookie    string       // 例: "lang"(空なら無効)
      24              :         SetHeader bool         // Content-Language を付与するか(既定: true)
      25              :         Store     LocaleSource // 自動検出用(Allow が空のとき推奨)
      26              : }
      27              : 
      28              : // CtxLocaleKey は Gin Context に格納するキー名
      29              : const CtxLocaleKey = "lang"
      30              : 
      31              : // Middleware は、Header > Query > Cookie > Default の順にロケールを決定し、
      32              : // Context にセットします(必要なら Response Header へ Content-Language も付与)。
      33            2 : func Middleware(o Opts) gin.HandlerFunc {
      34            3 :         if o.Header == "" {
      35            1 :                 o.Header = "Accept-Language"
      36            1 :         }
      37              :         // 旧実装同様、明示指定されなければ true
      38            3 :         if !o.SetHeader {
      39            1 :                 o.SetHeader = true
      40            1 :         }
      41            4 :         return func(c *gin.Context) {
      42            2 :                 loc := Negotiate(c.Request, o)
      43            4 :                 if o.SetHeader {
      44            2 :                         c.Writer.Header().Set("Content-Language", loc)
      45            2 :                 }
      46            2 :                 c.Set(CtxLocaleKey, loc)
      47            2 :                 c.Next()
      48              :         }
      49              : }
      50              : 
      51              : // From は Context からロケールを取り出すヘルパーです。
      52            1 : func From(c *gin.Context) string {
      53            2 :         if v, ok := c.Get(CtxLocaleKey); ok {
      54            2 :                 if s, ok := v.(string); ok {
      55            1 :                         return s
      56            1 :                 }
      57              :         }
      58            0 :         return ""
      59              : }
      60              : 
      61              : // Negotiate は HTTP リクエストからロケールを決定して返します。
      62              : // 優先順位:Query > Header > Cookie > Default
      63            2 : func Negotiate(r *http.Request, o Opts) string {
      64            3 :         inAllow := func(code string) bool {
      65            1 :                 code = strings.ToLower(strings.TrimSpace(code))
      66            1 :                 if code == "" {
      67            0 :                         return false
      68            0 :                 }
      69              :                 // Allow 未指定なら Store から検出
      70            1 :                 allow := o.Allow
      71            1 :                 if len(allow) == 0 && o.Store != nil {
      72            0 :                         allow = o.Store.AvailableLocaleCodes()
      73            0 :                 }
      74            1 :                 if len(allow) == 0 {
      75            0 :                         return true // 制限なし
      76            0 :                 }
      77            2 :                 for _, a := range allow {
      78            2 :                         if strings.EqualFold(a, code) {
      79            1 :                                 return true
      80            1 :                         }
      81              :                 }
      82            1 :                 return false
      83              :         }
      84              : 
      85              :         // 1) Query
      86            4 :         if qk := strings.TrimSpace(o.Query); qk != "" {
      87            3 :                 if v := strings.TrimSpace(r.URL.Query().Get(qk)); v != "" && inAllow(v) {
      88            1 :                         return strings.ToLower(v)
      89            1 :                 }
      90              :         }
      91              : 
      92              :         // 2) Header(先頭、地域は落として base を試す)
      93            4 :         if o.Header != "" {
      94            2 :                 al := r.Header.Get(o.Header)
      95            3 :                 if al != "" {
      96            2 :                         for _, part := range strings.Split(al, ",") {
      97            1 :                                 raw := strings.TrimSpace(strings.Split(part, ";")[0])
      98            1 :                                 if raw == "" {
      99            0 :                                         continue
     100              :                                 }
     101            1 :                                 raw = strings.ToLower(raw)
     102            1 :                                 // そのまま
     103            1 :                                 if inAllow(raw) {
     104            0 :                                         return raw
     105            0 :                                 }
     106              :                                 // サブタグ切り落とし(例: en-US -> en)
     107            2 :                                 if i := strings.IndexByte(raw, '-'); i > 0 {
     108            1 :                                         base := raw[:i]
     109            2 :                                         if inAllow(base) {
     110            1 :                                                 return base
     111            1 :                                         }
     112              :                                 }
     113              :                         }
     114              :                 }
     115              :         }
     116              : 
     117              :         // 3) Cookie
     118            2 :         if ck := strings.TrimSpace(o.Cookie); ck != "" {
     119            0 :                 if c, err := r.Cookie(ck); err == nil && c != nil {
     120            0 :                         if v := strings.TrimSpace(c.Value); v != "" && inAllow(v) {
     121            0 :                                 return strings.ToLower(v)
     122            0 :                         }
     123              :                 }
     124              :         }
     125              : 
     126              :         // 4) Default(最終フォールバック)
     127            2 :         return strings.ToLower(strings.TrimSpace(o.Default))
     128              : }
     129              : 
     130              : // Setter は Cookie を使ってロケールを保存するためのハンドラです。
     131              : // 例: GET /lang?to=en
     132            0 : func Setter(o Opts) gin.HandlerFunc {
     133            0 :         return func(c *gin.Context) {
     134            0 :                 to := strings.TrimSpace(c.Query("to"))
     135            0 :                 if to == "" {
     136            0 :                         to = o.Default
     137            0 :                 }
     138            0 :                 http.SetCookie(c.Writer, &http.Cookie{
     139            0 :                         Name:     o.Cookie,
     140            0 :                         Value:    to,
     141            0 :                         Path:     "/",
     142            0 :                         HttpOnly: true,
     143            0 :                         MaxAge:   86400 * 365,
     144            0 :                 })
     145            0 :                 c.JSON(http.StatusOK, gin.H{"ok": true, "locale": to})
     146              :         }
     147              : }
        

Generated by: LCOV version 2.3.1-1