├── .gitignore ├── mise.toml ├── version.go ├── go.mod ├── detectlanguage_test.go ├── languages_test.go ├── account_test.go ├── languages.go ├── client_test.go ├── CHANGELOG.md ├── .github └── workflows │ └── main.yml ├── error_test.go ├── account.go ├── error.go ├── go.sum ├── LICENSE ├── detect_test.go ├── detect.go ├── client.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env.local 2 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | 3 | [env] 4 | _.file = ".env.local" 5 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package detectlanguage 2 | 3 | // Version is the API client version 4 | const Version = "2.0.0" 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/detectlanguage/detectlanguage-go 2 | 3 | go 1.13 4 | 5 | require github.com/stretchr/testify v1.7.1 6 | -------------------------------------------------------------------------------- /detectlanguage_test.go: -------------------------------------------------------------------------------- 1 | package detectlanguage_test 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/detectlanguage/detectlanguage-go" 7 | ) 8 | 9 | var client = detectlanguage.New(os.Getenv("DETECTLANGUAGE_API_KEY")) 10 | -------------------------------------------------------------------------------- /languages_test.go: -------------------------------------------------------------------------------- 1 | package detectlanguage_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLanguages(t *testing.T) { 11 | languages, err := client.Languages(context.TODO()) 12 | 13 | if assert.NoError(t, err) { 14 | assert.NotEmpty(t, languages[0].Code) 15 | assert.NotEmpty(t, languages[0].Name) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /account_test.go: -------------------------------------------------------------------------------- 1 | package detectlanguage_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAccountStatus(t *testing.T) { 11 | response, err := client.AccountStatus(context.TODO()) 12 | 13 | if assert.NoError(t, err) { 14 | assert.NotEmpty(t, response.Date) 15 | assert.Equal(t, "ACTIVE", response.Status) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /languages.go: -------------------------------------------------------------------------------- 1 | package detectlanguage 2 | 3 | import "context" 4 | 5 | // Language is the resource representing language 6 | type Language struct { 7 | Code string `json:"code"` 8 | Name string `json:"name"` 9 | } 10 | 11 | // Languages retrieves the list of supported languages 12 | func (c *Client) Languages(ctx context.Context) (out []*Language, err error) { 13 | err = c.get(ctx, "languages", &out) 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package detectlanguage 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestClientBaseURL(t *testing.T) { 11 | url, _ := url.Parse("http://localhost") 12 | client := Client{BaseURL: url} 13 | assert.Equal(t, url, client.baseURL()) 14 | } 15 | 16 | func TestClientUserAgent(t *testing.T) { 17 | userAgent := "Test agent" 18 | client := Client{UserAgent: userAgent} 19 | assert.Equal(t, userAgent, client.userAgent()) 20 | } 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | ## v2.0.0 10 | 11 | ### Added 12 | - `context` to all API methods 13 | 14 | ### Changed 15 | - Switched to v3 API which uses updated language detection model 16 | - Language detection results contains `Language` and `Score` 17 | - `UserStatus()` renamed to `AccountStatus()` 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push,pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | go-version: 9 | - "1.14" 10 | - "1.17" 11 | - "1.18" 12 | - "1.20" 13 | - "1.22" 14 | - "1.24" 15 | - "1.25" 16 | name: Go ${{ matrix.go-version }} sample 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-go@v3 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | check-latest: true 23 | - env: 24 | DETECTLANGUAGE_API_KEY: ${{ secrets.DETECTLANGUAGE_API_KEY }} 25 | run: go test 26 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package detectlanguage_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/detectlanguage/detectlanguage-go" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAPIErrorWithoutMessage(t *testing.T) { 11 | err := detectlanguage.APIError{StatusCode: 404, Status: "Not found"} 12 | assert.Equal(t, "404 Not found", err.Error()) 13 | } 14 | 15 | func TestAPIErrorWithMessage(t *testing.T) { 16 | err := detectlanguage.APIError{ 17 | StatusCode: 401, 18 | Status: "Unauthorized", 19 | Code: 1, 20 | Message: "Invalid API key", 21 | } 22 | 23 | assert.Equal(t, "Invalid API key", err.Error()) 24 | } 25 | 26 | func TestDetectionError(t *testing.T) { 27 | err := detectlanguage.DetectionError{"Test error"} 28 | assert.Equal(t, "Test error", err.Error()) 29 | } 30 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | package detectlanguage 2 | 3 | import "context" 4 | 5 | // AccountStatusResponse is the resource containing account status information 6 | type AccountStatusResponse struct { 7 | Date string `json:"date,omitempty"` 8 | Requests int `json:"requests"` 9 | Bytes int `json:"bytes"` 10 | Plan string `json:"plan"` 11 | PlanExpires string `json:"plan_expires,omitempty"` 12 | DailyRequestsLimit int `json:"daily_requests_limit"` 13 | DailyBytesLimit int `json:"daily_bytes_limit"` 14 | Status string `json:"status"` 15 | } 16 | 17 | // AccountStatus fetches account status 18 | func (c *Client) AccountStatus(ctx context.Context) (out *AccountStatusResponse, err error) { 19 | err = c.get(ctx, "account/status", &out) 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package detectlanguage 2 | 3 | import "fmt" 4 | 5 | // APIError is an error returned from the API 6 | type APIError struct { 7 | // Status is the HTTP text status of the response. 8 | Status string 9 | // StatusCode is the HTTP numerical status code of the response. 10 | StatusCode int 11 | // Code is an error code returned by the API. 12 | Code int `json:"code"` 13 | // Message is a human-readable error code returned by the API. 14 | Message string `json:"message"` 15 | } 16 | 17 | func (e *APIError) Error() string { 18 | if e.Message != "" { 19 | return e.Message 20 | } 21 | return fmt.Sprintf("%d %s", e.StatusCode, e.Status) 22 | } 23 | 24 | type apiErrorResponse struct { 25 | Error *APIError `json:"error,omitempty"` 26 | } 27 | 28 | // DetectionError is retuned when detection fails 29 | type DetectionError struct { 30 | Message string 31 | } 32 | 33 | func (e *DetectionError) Error() string { 34 | return e.Message 35 | } 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 7 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Laurynas Butkus 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /detect_test.go: -------------------------------------------------------------------------------- 1 | package detectlanguage_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDetect(t *testing.T) { 11 | detections, err := client.Detect(context.TODO(), "labas rytas") 12 | 13 | if assert.NoError(t, err) { 14 | assert.Equal(t, "lt", detections[0].Language) 15 | assert.Greater(t, detections[0].Score, float64(0)) 16 | } 17 | } 18 | 19 | func TestDetectCode(t *testing.T) { 20 | code, err := client.DetectCode(context.TODO(), "labas rytas") 21 | 22 | if assert.NoError(t, err) { 23 | assert.Equal(t, "lt", code) 24 | } 25 | } 26 | 27 | func TestDetectCodeFailure(t *testing.T) { 28 | code, err := client.DetectCode(context.TODO(), " ") 29 | 30 | assert.EqualError(t, err, "Language not detected") 31 | assert.Equal(t, code, "") 32 | } 33 | 34 | func TestDetectBatch(t *testing.T) { 35 | query := []string{"labas rytas", "good morning"} 36 | detections, err := client.DetectBatch(context.TODO(), query) 37 | 38 | if assert.NoError(t, err) { 39 | assert.Equal(t, "lt", detections[0][0].Language) 40 | assert.Equal(t, "en", detections[1][0].Language) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /detect.go: -------------------------------------------------------------------------------- 1 | package detectlanguage 2 | 3 | import "context" 4 | 5 | // DetectRequest contains language detection request params 6 | type DetectRequest struct { 7 | Query string `json:"q"` 8 | } 9 | 10 | // DetectionResult is single language detection result 11 | type DetectionResult struct { 12 | Language string `json:"language"` 13 | Score float64 `json:"score"` 14 | } 15 | 16 | // DetectBatchRequest contains batch language detection request params 17 | type DetectBatchRequest struct { 18 | Query []string `json:"q"` 19 | } 20 | 21 | // Detect executes language detection for a single text 22 | func (c *Client) Detect(ctx context.Context, in string) (out []*DetectionResult, err error) { 23 | var response []*DetectionResult 24 | err = c.post(ctx, "detect", &DetectRequest{Query: in}, &response) 25 | 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return response, err 31 | } 32 | 33 | // DetectCode executes language detection for a single text and returns detected language code 34 | func (c *Client) DetectCode(ctx context.Context, in string) (out string, err error) { 35 | detections, err := c.Detect(ctx, in) 36 | 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | if len(detections) == 0 { 42 | return "", &DetectionError{"Language not detected"} 43 | } 44 | 45 | return detections[0].Language, err 46 | } 47 | 48 | // DetectBatch executes language detection with multiple texts. 49 | // It is significantly faster than doing a separate request for each text indivdually. 50 | func (c *Client) DetectBatch(ctx context.Context, in []string) (out [][]*DetectionResult, err error) { 51 | var response [][]*DetectionResult 52 | err = c.post(ctx, "detect-batch", &DetectBatchRequest{Query: in}, &response) 53 | 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return response, err 59 | } 60 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package detectlanguage 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "time" 12 | ) 13 | 14 | const defaultUserAgent = "detectlanguage-go/" + Version 15 | 16 | const defaultTimeout = 10 * time.Second 17 | 18 | var apiBaseURL = &url.URL{ 19 | Scheme: "https", Host: "ws.detectlanguage.com", Path: "/v3/", 20 | } 21 | 22 | // A Client provides an HTTP client for DetectLanguage API operations. 23 | type Client struct { 24 | // BaseURL specifies the location of the API. It is used with 25 | // ResolveReference to create request URLs. (If 'Path' is specified, it 26 | // should end with a trailing slash.) If nil, the default will be used. 27 | BaseURL *url.URL 28 | // Client is an HTTP client used to make API requests. If nil, 29 | // default will be used. 30 | Client *http.Client 31 | // APIKey is the user's API key. It is required. 32 | // Note: Treat your API Keys as passwords—keep them secret. API Keys give 33 | // full read/write access to your account, so they should not be included in 34 | // public repositories, emails, client side code, etc. 35 | APIKey string 36 | // UserAgent is a User-Agent to be sent with API HTTP requests. If empty, 37 | // a default will be used. 38 | UserAgent string 39 | } 40 | 41 | // New returns a new Client with the given API key. 42 | func New(apiKey string) *Client { 43 | return &Client{APIKey: apiKey} 44 | } 45 | 46 | func (c *Client) baseURL() *url.URL { 47 | if c.BaseURL != nil { 48 | return c.BaseURL 49 | } 50 | return apiBaseURL 51 | } 52 | 53 | func (c *Client) userAgent() string { 54 | if c.UserAgent != "" { 55 | return c.UserAgent 56 | } 57 | return defaultUserAgent 58 | } 59 | 60 | func (c *Client) client() *http.Client { 61 | if c.Client == nil { 62 | c.Client = &http.Client{Timeout: defaultTimeout} 63 | } 64 | return c.Client 65 | } 66 | 67 | func (c *Client) setBody(req *http.Request, in interface{}) error { 68 | if in != nil { 69 | buf, err := json.Marshal(in) 70 | if err != nil { 71 | return err 72 | } 73 | req.Body = ioutil.NopCloser(bytes.NewReader(buf)) 74 | req.GetBody = func() (io.ReadCloser, error) { 75 | return ioutil.NopCloser(bytes.NewReader(buf)), nil 76 | } 77 | req.Header.Set("Content-Type", "application/json") 78 | req.ContentLength = int64(len(buf)) 79 | } 80 | return nil 81 | } 82 | 83 | func (c *Client) do(ctx context.Context, method, path string, in, out interface{}) error { 84 | req := &http.Request{ 85 | Method: method, 86 | URL: c.baseURL().ResolveReference(&url.URL{Path: path}), 87 | Header: make(http.Header, 2), 88 | } 89 | req.Header.Set("User-Agent", c.userAgent()) 90 | if err := c.setBody(req, in); err != nil { 91 | return err 92 | } 93 | req.Header.Add("Authorization", "Bearer "+c.APIKey) 94 | if ctx != nil { 95 | req = req.WithContext(ctx) 96 | } 97 | res, err := c.client().Do(req) 98 | if err != nil { 99 | return err 100 | } 101 | defer res.Body.Close() 102 | 103 | if res.StatusCode >= 200 && res.StatusCode <= 299 { 104 | if out != nil { 105 | return json.NewDecoder(res.Body).Decode(out) 106 | } 107 | return nil 108 | } 109 | 110 | buf, _ := ioutil.ReadAll(res.Body) 111 | apiErr := &APIError{Status: res.Status, StatusCode: res.StatusCode} 112 | if json.Unmarshal(buf, &apiErrorResponse{Error: apiErr}) != nil { 113 | apiErr.Message = string(buf) 114 | } 115 | return apiErr 116 | } 117 | 118 | func (c *Client) get(ctx context.Context, path string, out interface{}) error { 119 | return c.do(ctx, http.MethodGet, path, nil, out) 120 | } 121 | 122 | func (c *Client) post(ctx context.Context, path string, in, out interface{}) error { 123 | return c.do(ctx, http.MethodPost, path, in, out) 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Detect Language API Go Client 2 | ======== 3 | 4 | [![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/detectlanguage/detectlanguage-go) 5 | [![Build Status](https://github.com/detectlanguage/detectlanguage-go/actions/workflows/main.yml/badge.svg)](https://github.com/detectlanguage/detectlanguage-go/actions) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/detectlanguage/detectlanguage-go)](https://goreportcard.com/report/github.com/detectlanguage/detectlanguage-go) 7 | 8 | Detects language of the given text. Returns detected language codes and scores. 9 | 10 | Before using Detect Language API client you have to setup your personal API key. 11 | You can get it by signing up at https://detectlanguage.com 12 | 13 | ## Installation 14 | 15 | ``` 16 | go get -u github.com/detectlanguage/detectlanguage-go@v2.0.0 17 | ``` 18 | 19 | ### Configuration 20 | 21 | ```go 22 | dl := detectlanguage.New("YOUR API KEY") 23 | ``` 24 | 25 | ## Usage 26 | 27 | ### Language detection 28 | 29 | ```go 30 | detections, err := dl.Detect(context.TODO(), "Dolce far niente") 31 | 32 | if err != nil { 33 | fmt.Fprintln(os.Stderr, "error detecting language:", err) 34 | os.Exit(1) 35 | return 36 | } 37 | 38 | fmt.Fprintln(os.Stdout, "Language:", detections[0].Language) 39 | fmt.Fprintln(os.Stdout, "Score:", detections[0].Score) 40 | ``` 41 | 42 | ### Single language code detection 43 | 44 | If you need just a language code you can use `DetectCode`. It returns first detected language code. 45 | 46 | ```go 47 | language, err := dl.DetectCode(context.TODO(), "Dolce far niente") 48 | 49 | if err != nil { 50 | fmt.Fprintln(os.Stderr, "error detecting language:", err) 51 | os.Exit(1) 52 | return 53 | } 54 | 55 | fmt.Fprintln(os.Stdout, "Language:", language) 56 | ``` 57 | 58 | ### Batch detection 59 | 60 | It is possible to detect language of several texts with one request. 61 | This method is significantly faster than doing one request per text. 62 | To use batch detection just pass multiple texts to `DetectBatch` method. 63 | 64 | ```go 65 | texts := []string{"labas rytas", "good morning"} 66 | results, err := dl.DetectBatch(context.TODO(), texts) 67 | 68 | if err != nil { 69 | fmt.Fprintln(os.Stderr, "error detecting language:", err) 70 | os.Exit(1) 71 | return 72 | } 73 | 74 | fmt.Fprintln(os.Stdout, "First text language:", detections[0][0].Language) 75 | fmt.Fprintln(os.Stdout, "Second text language:", detections[1][0].Language) 76 | ``` 77 | 78 | ### Getting your account status 79 | 80 | ```go 81 | result, err := dl.AccountStatus(context.TODO()) 82 | 83 | if err != nil { 84 | fmt.Fprintln(os.Stderr, "error getting account status:", err) 85 | os.Exit(1) 86 | return 87 | } 88 | 89 | fmt.Fprintln(os.Stdout, "Status:", result.Status) 90 | fmt.Fprintln(os.Stdout, "Requests sent today:", result.Requests) 91 | fmt.Fprintln(os.Stdout, "Bytes sent today:", result.Bytes) 92 | fmt.Fprintln(os.Stdout, "Plan:", result.Plan) 93 | fmt.Fprintln(os.Stdout, "Plan expires:", result.PlanExpires) 94 | fmt.Fprintln(os.Stdout, "Daily requests limit:", result.DailyRequestsLimit) 95 | fmt.Fprintln(os.Stdout, "Daily bytes limit:", result.DailyBytesLimit) 96 | fmt.Fprintln(os.Stdout, "Date:", result.Date) 97 | ``` 98 | 99 | ### Getting list supported languages 100 | 101 | ```go 102 | languages, err := dl.Languages(context.TODO()) 103 | 104 | if err != nil { 105 | fmt.Fprintln(os.Stderr, "error getting languages list:", err) 106 | os.Exit(1) 107 | return 108 | } 109 | 110 | fmt.Fprintln(os.Stdout, "Supported languages:", len(languages)) 111 | fmt.Fprintln(os.Stdout, "First language code:", languages[0].Code) 112 | fmt.Fprintln(os.Stdout, "First language name:", languages[0].Name) 113 | ``` 114 | 115 | ## License 116 | 117 | Detect Language API Go Client is free software, and may be redistributed under the terms specified in the MIT-LICENSE file. 118 | --------------------------------------------------------------------------------