Places: Add config option to specify location details locale #465 #883

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-07-03 12:58:20 +02:00
parent 4b1cc5d424
commit 02d8525ade
30 changed files with 203 additions and 83 deletions

10
NOTICE
View File

@@ -9,7 +9,7 @@ The following 3rd-party software packages may be used by or distributed with
PhotoPrism. Any information relevant to third-party vendors listed below are
collected using common, reasonable means.
Date generated: 2025-07-02
Date generated: 2025-07-03
================================================================================
@@ -2343,8 +2343,8 @@ SOFTWARE.
--------------------------------------------------------------------------------
Package: github.com/golang/geo
Version: v0.0.0-20250627182359-f4b81656db99
License: Apache-2.0 (https://github.com/golang/geo/blob/f4b81656db99/LICENSE)
Version: v0.0.0-20250702194635-55051eb0594d
License: Apache-2.0 (https://github.com/golang/geo/blob/55051eb0594d/LICENSE)
Apache License
@@ -6414,8 +6414,8 @@ License: Apache-2.0 (https://github.com/zitadel/logging/blob/v0.6.2/LICENSE)
--------------------------------------------------------------------------------
Package: github.com/zitadel/oidc/v3/pkg
Version: v3.39.0
License: Apache-2.0 (https://github.com/zitadel/oidc/blob/v3.39.0/LICENSE)
Version: v3.39.1
License: Apache-2.0 (https://github.com/zitadel/oidc/blob/v3.39.1/LICENSE)
Apache License
Version 2.0, January 2004

4
go.mod
View File

@@ -14,7 +14,7 @@ require (
github.com/esimov/pigo v1.4.6
github.com/gin-contrib/gzip v1.2.3
github.com/gin-gonic/gin v1.10.1
github.com/golang/geo v0.0.0-20250630213057-fac2d31592dd
github.com/golang/geo v0.0.0-20250702194635-55051eb0594d
github.com/google/open-location-code/go v0.0.0-20250620134813-83986da0156b
github.com/gorilla/websocket v1.5.3
github.com/gosimple/slug v1.15.0
@@ -86,7 +86,7 @@ require (
github.com/ugjka/go-tz/v2 v2.2.6
github.com/urfave/cli/v2 v2.27.7
github.com/wamuir/graft v0.10.0
github.com/zitadel/oidc/v3 v3.39.0
github.com/zitadel/oidc/v3 v3.39.1
golang.org/x/mod v0.25.0
golang.org/x/sys v0.33.0
)

12
go.sum
View File

@@ -133,8 +133,8 @@ github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-co-op/gocron/v2 v2.16.2 h1:r08P663ikXiulLT9XaabkLypL/W9MoCIbqgQoAutyX4=
github.com/go-co-op/gocron/v2 v2.16.2/go.mod h1:4YTLGCCAH75A5RlQ6q+h+VacO7CgjkgP0EJ+BEOXRSI=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@@ -184,8 +184,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20250630213057-fac2d31592dd h1:4BU/Fy2KwDucgY0al91L0wJI05rb1Y4hQKucQkybjT8=
github.com/golang/geo v0.0.0-20250630213057-fac2d31592dd/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
github.com/golang/geo v0.0.0-20250702194635-55051eb0594d h1:AzT7tYHcPLcUlo6224yQJvhL1A9GjT5g1cYbyDSYt/w=
github.com/golang/geo v0.0.0-20250702194635-55051eb0594d/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -419,8 +419,8 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBi
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
github.com/zitadel/oidc/v3 v3.39.0 h1:WK3eNqmgshiYo1oEqONfXXbPbve+Qzgjl8KhKDFUvxc=
github.com/zitadel/oidc/v3 v3.39.0/go.mod h1:JwdgdU/WxkmBtWuE8/pEjAbDTWXxJGqBix/gUoeEig4=
github.com/zitadel/oidc/v3 v3.39.1 h1:6QwGwI3yxh4somT7fwRCeT1KOn/HOGv0PA0dFciwJjE=
github.com/zitadel/oidc/v3 v3.39.1/go.mod h1:aH8brOrzoliAybVdfq2xIdGvbtl0j/VsKRNa7WE72gI=
github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU=
github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=

View File

@@ -13,5 +13,5 @@ func ClientIP(c *gin.Context) (ip string) {
// UserAgent returns the user agent from the request context or an empty string if it is unknown.
func UserAgent(c *gin.Context) string {
return header.UserAgent(c)
return header.ClientUserAgent(c)
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/internal/service/hub/places"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -19,12 +20,13 @@ import (
// @Id GetPlacesReverse
// @Tags Places
// @Produce json
// @Param lat query string true "Latitude"
// @Param lng query string true "Longitude"
// @Success 200 {object} places.Location
// @Failure 400 {object} gin.H "Missing latitude or longitude"
// @Failure 401 {object} i18n.Response
// @Failure 500 {object} gin.H "Geocoding service error"
// @Param lat query string true "Latitude"
// @Param lng query string true "Longitude"
// @Param locale query string false "Locale"
// @Success 200 {object} places.Location
// @Failure 400 {object} gin.H "Missing latitude or longitude"
// @Failure 401 {object} i18n.Response
// @Failure 500 {object} gin.H "Geocoding service error"
// @Router /api/v1/places/reverse [get]
func GetPlacesReverse(router *gin.RouterGroup) {
handler := func(c *gin.Context) {
@@ -44,7 +46,7 @@ func GetPlacesReverse(router *gin.RouterGroup) {
return
}
// Get latitude and longitude from query parameters.
// Get latitude, longitude, and locale from query parameters.
var lat, lng string
if lat = txt.Numeric(c.Query("lat")); lat == "" {
@@ -61,14 +63,19 @@ func GetPlacesReverse(router *gin.RouterGroup) {
return
}
result, err := places.LatLng(txt.Float64(lat), txt.Float64(lng))
locale := clean.WebLocale(c.Query("locale"), conf.PlacesLocale())
// Perform service request.
result, err := places.LatLng(txt.Float64(lat), txt.Float64(lng), locale)
// Return error if request was not successful.
if err != nil {
log.Errorf("places: failed to resolve location at lat %s, lng %s", lat, lng)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
return
}
// Return location details.
c.JSON(http.StatusOK, result)
}

View File

@@ -48,7 +48,7 @@ func GetPlacesSearch(router *gin.RouterGroup) {
// Get the search string, locale, and result count limit from the query parameters.
query := clean.SearchString(c.Query("q"))
locale := clean.WebLocale(c.Query("locale"), conf.DefaultLocale())
locale := clean.WebLocale(c.Query("locale"), conf.PlacesLocale())
count := txt.IntVal(c.Query("count"), 1, 50, 10)
if query == "" {

View File

@@ -3873,6 +3873,12 @@
"name": "lng",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Locale",
"name": "locale",
"in": "query"
}
],
"responses": {
@@ -5665,6 +5671,9 @@
"OriginalsLimit": {
"type": "integer"
},
"PlacesLocale": {
"type": "string"
},
"PngSize": {
"type": "integer"
},
@@ -7534,6 +7543,9 @@
"lng": {
"type": "number"
},
"locale": {
"type": "string"
},
"name": {
"type": "string"
},
@@ -7580,7 +7592,7 @@
"places.SearchResult": {
"type": "object",
"properties": {
"boundingbox": {
"bbox": {
"type": "array",
"items": {
"type": "number"

View File

@@ -306,6 +306,7 @@ func (c *Config) Propagate() {
// Set geocoding parameters.
places.UserAgent = c.UserAgent()
places.DefaultLocale = c.PlacesLocale()
entity.GeoApi = c.GeoApi()
// Set session cache duration.
@@ -698,15 +699,6 @@ func (c *Config) AutoImport() time.Duration {
return time.Duration(c.options.AutoImport) * time.Second
}
// GeoApi returns the preferred geocoding api (places, or none).
func (c *Config) GeoApi() string {
if c.options.DisablePlaces {
return ""
}
return "places"
}
// OriginalsLimit returns the maximum size of originals in MB.
func (c *Config) OriginalsLimit() int {
if c.options.OriginalsLimit <= 0 || c.options.OriginalsLimit > 100000 {

View File

@@ -0,0 +1,20 @@
package config
import (
"github.com/photoprism/photoprism/internal/service/hub/places"
"github.com/photoprism/photoprism/pkg/clean"
)
// GeoApi returns the preferred geocoding api (places, or none).
func (c *Config) GeoApi() string {
if c.options.DisablePlaces {
return ""
}
return "places"
}
// PlacesLocale returns the locale name used for geocoding.
func (c *Config) PlacesLocale() string {
return clean.WebLocale(c.options.PlacesLocale, places.LocalLocale)
}

View File

@@ -0,0 +1,30 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfig_GeoApi(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "places", c.GeoApi())
c.options.DisablePlaces = true
assert.Equal(t, "", c.GeoApi())
}
func TestConfig_PlacesLocale(t *testing.T) {
c := NewConfig(CliTestContext())
c.options.PlacesLocale = ""
assert.Equal(t, "local", c.PlacesLocale())
c.options.PlacesLocale = "local"
assert.Equal(t, "local", c.PlacesLocale())
c.options.PlacesLocale = "EN"
assert.Equal(t, "en", c.PlacesLocale())
c.options.PlacesLocale = "EN_US"
assert.Equal(t, "en-US", c.PlacesLocale())
c.options.PlacesLocale = ""
assert.Equal(t, "local", c.PlacesLocale())
}

View File

@@ -316,14 +316,6 @@ func TestConfig_AutoImport(t *testing.T) {
assert.Equal(t, 2*time.Hour, c.AutoImport())
}
func TestConfig_GeoApi(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "places", c.GeoApi())
c.options.DisablePlaces = true
assert.Equal(t, "", c.GeoApi())
}
func TestConfig_OriginalsLimit(t *testing.T) {
c := NewConfig(CliTestContext())

View File

@@ -10,6 +10,7 @@ import (
"github.com/photoprism/photoprism/internal/config/ttl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
"github.com/photoprism/photoprism/internal/service/hub/places"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/i18n"
@@ -510,6 +511,12 @@ var Flags = CliFlags{
Usage: "default user interface theme `NAME`",
EnvVars: EnvVars("DEFAULT_THEME"),
}}, {
Flag: &cli.StringFlag{
Name: "places-locale",
Usage: "location details language `CODE`, e.g. en, de, or local",
Value: places.LocalLocale,
EnvVars: EnvVars("PLACES_LOCALE"),
}}, {
Flag: &cli.StringFlag{
Name: "app-name",
Usage: "progressive web app `NAME` when installed on a device",

View File

@@ -118,6 +118,7 @@ type Options struct {
DefaultLocale string `yaml:"DefaultLocale" json:"DefaultLocale" flag:"default-locale"`
DefaultTimezone string `yaml:"DefaultTimezone" json:"DefaultTimezone" flag:"default-timezone"`
DefaultTheme string `yaml:"DefaultTheme" json:"DefaultTheme" flag:"default-theme"`
PlacesLocale string `yaml:"PlacesLocale" json:"PlacesLocale" flag:"places-locale"`
AppName string `yaml:"AppName" json:"AppName" flag:"app-name"`
AppMode string `yaml:"AppMode" json:"AppMode" flag:"app-mode"`
AppIcon string `yaml:"AppIcon" json:"AppIcon" flag:"app-icon"`

View File

@@ -139,6 +139,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"default-locale", c.DefaultLocale()},
{"default-timezone", c.DefaultTimezone().String()},
{"default-theme", c.DefaultTheme()},
{"places-locale", c.PlacesLocale()},
{"app-name", c.AppName()},
{"app-mode", c.AppMode()},
{"app-icon", c.AppIcon()},

View File

@@ -662,7 +662,7 @@ func (m *Session) SetContext(c *gin.Context) *Session {
}
// Set client user agent from request context.
if ua := header.UserAgent(c); ua != "" {
if ua := header.ClientUserAgent(c); ua != "" {
m.SetUserAgent(ua)
}
@@ -688,7 +688,7 @@ func (m *Session) UpdateContext(c *gin.Context) *Session {
}
// Set client user agent from request context.
if ua := header.UserAgent(c); ua != "" && ua != m.UserAgent {
if ua := header.ClientUserAgent(c); ua != "" && ua != m.UserAgent {
m.SetUserAgent(ua)
changed = true
}

View File

@@ -245,9 +245,9 @@ func (c *Config) ReSync(token string) (err error) {
// Set user agent.
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
req.Header.Set(header.UserAgent, c.UserAgent)
} else {
req.Header.Set("User-Agent", "PhotoPrism/Test")
req.Header.Set(header.UserAgent, "PhotoPrism/Test")
}
// Add Content-Type header.

View File

@@ -82,12 +82,12 @@ func (c *Config) SendFeedback(frm form.Feedback) (err error) {
// Set user agent.
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
req.Header.Set(header.UserAgent, c.UserAgent)
} else {
req.Header.Set("User-Agent", "PhotoPrism/Test")
req.Header.Set(header.UserAgent, "PhotoPrism/Test")
}
req.Header.Add("Accept-Language", frm.UserLocales)
req.Header.Add(header.AcceptLanguage, frm.UserLocales)
req.Header.Add(header.ContentType, header.ContentTypeJson)
var r *http.Response

View File

@@ -11,7 +11,7 @@ import (
)
// Cell returns location details based on the specified S2 cell ID.
func Cell(id string) (result Location, err error) {
func Cell(id string, locale string) (result Location, err error) {
// Normalize S2 Cell ID.
id = s2.NormalizeToken(id)
@@ -34,8 +34,11 @@ func Cell(id string) (result Location, err error) {
return result, fmt.Errorf("skipping lat %f, lng %f", lat, lng)
}
// Get request locale.
locale = Locale(locale)
// Create cache key based on query parameters.
cacheKey := fmt.Sprintf("id:%s", id)
cacheKey := fmt.Sprintf("id:%s:%s", id, locale)
// Location details cached?
if hit, ok := clientCache.Get(cacheKey); ok {
@@ -50,7 +53,7 @@ func Cell(id string) (result Location, err error) {
// Query the specified places service URLs.
for _, serviceUrl := range LocationServiceUrls {
reqUrl := fmt.Sprintf(serviceUrl, id)
if r, err = GetRequest(reqUrl); err == nil {
if r, err = GetRequest(reqUrl, locale); err == nil {
break
}
}

View File

@@ -14,7 +14,7 @@ func TestCell(t *testing.T) {
lng := 13.40806264572578
id := s2.Token(lat, lng)
l, err := Cell(id)
l, err := Cell(id, DefaultLocale)
if err != nil {
t.Fatal(err)
@@ -25,17 +25,17 @@ func TestCell(t *testing.T) {
assert.Equal(t, "de", l.CountryCode())
})
t.Run("MissingId", func(t *testing.T) {
l, err := Cell("")
l, err := Cell("", DefaultLocale)
assert.Error(t, err, "places: invalid location id ")
t.Log(l)
})
t.Run("WrongId", func(t *testing.T) {
l, err := Cell("2")
l, err := Cell("2", DefaultLocale)
assert.Error(t, err, "places: skipping lat 0.000000, lng 0.000000")
t.Log(l)
})
t.Run("ShortId", func(t *testing.T) {
l, err := Cell("ab")
l, err := Cell("ab", DefaultLocale)
assert.Error(t, err, "places: skipping lat 0.000000, lng 0.000000")
t.Log(l)
})
@@ -52,12 +52,12 @@ func TestCell(t *testing.T) {
Cached: true,
}
l, err := Cell(location.ID)
l, err := Cell(location.ID, DefaultLocale)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, false, l.Cached)
l2, err2 := Cell("1e95998417cc")
l2, err2 := Cell("1e95998417cc", DefaultLocale)
if err2 != nil {
t.Fatal(err2)

View File

@@ -12,7 +12,7 @@ import (
)
// LatLng returns location details based on the specified latitude and longitude.
func LatLng(lat, lng float64) (result Location, err error) {
func LatLng(lat, lng float64, locale string) (result Location, err error) {
if lat == 0.0 || lng == 0.0 {
return result, ErrMissingCoordinates
}
@@ -24,9 +24,12 @@ func LatLng(lat, lng float64) (result Location, err error) {
values := url.Values{"lat": {fmt.Sprintf("%f", lat)}, "lng": {fmt.Sprintf("%f", lng)}}
params := values.Encode()
// Get request locale.
locale = Locale(locale)
// Create cache key based on query parameters.
id := s2.Token(lat, lng)
cacheKey := fmt.Sprintf("id:%s", id)
cacheKey := fmt.Sprintf("id:%s:%s", id, locale)
// Are location results cached?
if hit, ok := clientCache.Get(cacheKey); ok {
@@ -41,7 +44,7 @@ func LatLng(lat, lng float64) (result Location, err error) {
// Query the specified places service URLs.
for _, serviceUrl := range ReverseServiceUrls {
reqUrl := fmt.Sprintf("%s?%s", serviceUrl, params)
if r, err = GetRequest(reqUrl); err == nil {
if r, err = GetRequest(reqUrl, locale); err == nil {
break
}
}

View File

@@ -7,26 +7,40 @@ import (
)
func TestLatLng(t *testing.T) {
lat := 52.51961810676184
lng := 13.40806264572578
lat := 52.5208
lng := 13.40953
t.Run("Success", func(t *testing.T) {
l, err := LatLng(lat, lng)
t.Run("Local", func(t *testing.T) {
l, err := LatLng(lat, lng, LocalLocale)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, LocalLocale, l.Locale)
assert.Equal(t, "Berliner Fernsehturm", l.Name())
assert.Equal(t, "Berlin", l.City())
assert.Equal(t, "de", l.CountryCode())
})
t.Run("Englisb", func(t *testing.T) {
l, err := LatLng(lat, lng, "en")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "en", l.Locale)
assert.Equal(t, "Fernsehturm Berlin", l.Name())
assert.Equal(t, "Berlin", l.City())
assert.Equal(t, "de", l.CountryCode())
})
t.Run("MissingLng", func(t *testing.T) {
l, err := LatLng(1, 0)
l, err := LatLng(1, 0, LocalLocale)
assert.Error(t, err, "places: skipping lat 0.000000, lng 0.000000")
t.Log(l)
})
t.Run("MissingLat", func(t *testing.T) {
l, err := LatLng(0, 1)
l, err := LatLng(0, 1, LocalLocale)
assert.Error(t, err, "places: skipping lat 0.000000, lng 0.000000")
t.Log(l)
})
@@ -43,13 +57,13 @@ func TestLatLng(t *testing.T) {
Cached: true,
}
_, err := LatLng(location.LocLat, location.LocLng)
_, err := LatLng(location.LocLat, location.LocLng, LocalLocale)
if err != nil {
t.Fatal(err)
}
cachedLoc, cacheErr := LatLng(location.LocLat, location.LocLng)
cachedLoc, cacheErr := LatLng(location.LocLat, location.LocLng, LocalLocale)
if cacheErr != nil {
t.Fatal(cacheErr)

View File

@@ -0,0 +1,16 @@
package places
import (
"github.com/photoprism/photoprism/pkg/clean"
)
// LocalLocale specifies the locale name to return results in the local language.
const LocalLocale = "local"
// DefaultLocale specifies the default places query locale.
var DefaultLocale = LocalLocale
// Locale returns the places query locale string.
func Locale(locale string) string {
return clean.WebLocale(locale, DefaultLocale)
}

View File

@@ -10,6 +10,7 @@ import (
// Location represents a specific geolocation identified by its S2 ID.
type Location struct {
ID string `json:"id"`
Locale string `json:"locale,omitempty"`
LocLat float64 `json:"lat"`
LocLng float64 `json:"lng"`
LocName string `json:"name"`

View File

@@ -5,10 +5,12 @@ import (
"fmt"
"net/http"
"time"
"github.com/photoprism/photoprism/pkg/media/http/header"
)
// GetRequest fetches the cell ID data from the service URL.
func GetRequest(reqUrl string) (r *http.Response, err error) {
func GetRequest(reqUrl string, locale string) (r *http.Response, err error) {
var req *http.Request
// Log request URL.
@@ -25,9 +27,14 @@ func GetRequest(reqUrl string) (r *http.Response, err error) {
// Set user agent.
if UserAgent != "" {
req.Header.Set("User-Agent", UserAgent)
req.Header.Set(header.UserAgent, UserAgent)
} else {
req.Header.Set("User-Agent", "PhotoPrism/Test")
req.Header.Set(header.UserAgent, "PhotoPrism/Test")
}
// Set requested result locale.
if locale != "" {
req.Header.Set(header.AcceptLanguage, locale)
}
// Add API key?

View File

@@ -31,15 +31,13 @@ func Search(q, locale string, count int) (results SearchResults, err error) {
// Generate query parameter string.
values := url.Values{"q": {q}, "count": {strconv.Itoa(count)}}
if locale != "" {
values.Add("locale", locale)
}
params := values.Encode()
// Get request locale.
locale = Locale(locale)
// Create cache key based on query parameters.
cacheKey := fmt.Sprintf("search:%s", params)
cacheKey := fmt.Sprintf("search:%s:%s", params, locale)
// Are location results cached?
if hit, ok := clientCache.Get(cacheKey); ok {
@@ -53,7 +51,7 @@ func Search(q, locale string, count int) (results SearchResults, err error) {
// Query the specified places service URLs.
for _, serviceUrl := range SearchServiceUrls {
reqUrl := fmt.Sprintf("%s?%s", serviceUrl, params)
if r, err = GetRequest(reqUrl); err == nil {
if r, err = GetRequest(reqUrl, locale); err == nil {
break
}
}

View File

@@ -7,7 +7,7 @@ type SearchResult struct {
Country string `json:"country"`
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
Boundingbox []float64 `json:"boundingbox,omitempty"`
BoundingBox []float64 `json:"bbox,omitempty"`
Importance float64 `json:"importance,omitempty"`
Licence string `json:"licence,omitempty"`
}

View File

@@ -52,7 +52,7 @@ func (l *Location) QueryApi(api string) error {
}
func (l *Location) QueryPlaces() error {
s, err := places.Cell(l.ID)
s, err := places.Cell(l.ID, places.DefaultLocale)
if err != nil {
return err

View File

@@ -43,6 +43,13 @@ func TestPosixLocale(t *testing.T) {
assert.Equal(t, "de", PosixLocale("und", "de"))
assert.Equal(t, "cs", PosixLocale("cs", "und"))
})
t.Run("Local", func(t *testing.T) {
assert.Equal(t, "local", PosixLocale("", "local"))
assert.Equal(t, "Local", PosixLocale("", "Local"))
assert.Equal(t, "", PosixLocale("local", ""))
assert.Equal(t, "", PosixLocale("Local", ""))
assert.Equal(t, "local", PosixLocale("local", "local"))
})
t.Run("Territory", func(t *testing.T) {
assert.Equal(t, "cs_CZ", PosixLocale("cs_CZ", ""))
assert.Equal(t, "cs_CZ", PosixLocale("cs-CZ", ""))
@@ -68,6 +75,13 @@ func TestWebLocale(t *testing.T) {
assert.Equal(t, "de", WebLocale("und", "de"))
assert.Equal(t, "cs", WebLocale("cs", "und"))
})
t.Run("Local", func(t *testing.T) {
assert.Equal(t, "local", WebLocale("", "local"))
assert.Equal(t, "Local", WebLocale("", "Local"))
assert.Equal(t, "", WebLocale("local", ""))
assert.Equal(t, "", WebLocale("Local", ""))
assert.Equal(t, "local", WebLocale("local", "local"))
})
t.Run("Territory", func(t *testing.T) {
assert.Equal(t, "cs-CZ", WebLocale("cs-CZ", ""))
assert.Equal(t, "cs-CZ", WebLocale("cs_CZ", ""))

View File

@@ -11,6 +11,7 @@ const (
Browser = "Sec-Ch-Ua"
Platform = "Sec-Ch-Ua-Platform"
FetchMode = "Sec-Fetch-Mode"
UserAgent = "User-Agent"
)
// Standard IP addresses and placeholders.
@@ -36,8 +37,9 @@ func ClientIP(c *gin.Context) (ip string) {
return UnknownIP
}
// UserAgent returns the user agent from the request context or an empty string if it is unknown.
func UserAgent(c *gin.Context) string {
// ClientUserAgent returns the client user agent string
// from the request context, or an empty string if unknown.
func ClientUserAgent(c *gin.Context) string {
if c == nil {
// Should never happen.
return ""

View File

@@ -32,7 +32,7 @@ func TestRequest(t *testing.T) {
Cookie: []string{"CockpitLang=en-us; Foo=Bar"},
},
}
assert.Equal(t, "TEST", UserAgent(c))
assert.Equal(t, "TEST", ClientUserAgent(c))
assert.Equal(t, "\"Chromium\";v=\"130\", \"Google Chrome\";v=\"130\", \"Not?A_Brand\";v=\"99\"", c.GetHeader(Browser))
assert.Equal(t, "\"Linux\"", c.GetHeader(Platform))
assert.Equal(t, "navigate", c.GetHeader(FetchMode))