Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## [0.3.0] - 2026-02-19

No breaking changes

### Added
- feat: `detect_language` function for detecting the language of a single string of text
- feat: `detect_language` function for detecting the language of multiple strings of text
- feat: `get_languages` function to retrieve the list of supported source or target languages from the DeepL API
- feat: `CLAUDE.md` file

### Notes
Since the DeepL API does not provide a dedicated language detection endpoint, `detect_language` uses the translate endpoint internally and extracts the `detected_source_language` field from the response.

## [0.2.0] - 2025-06-03

Expand Down
52 changes: 52 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# CLAUDE.md

## Project Overview

DeepL.jl is a Julia SDK for the DeepL translation API. It provides functions to translate text, detect languages, and list supported languages.

## Quick Reference

```bash
# Install dependencies
julia --project=. -e 'using Pkg; Pkg.instantiate()'

# Run tests (requires DEEPL_API_KEY env var)
julia --project=. -e 'using Pkg; Pkg.test()'
```

## Project Structure

```
src/
DeepL.jl # Main module — exports, constants, includes
client.jl # HTTP helpers (post_request, get_request)
translate.jl # translate_text + language constant lists
detect.jl # detect_language
languages.jl # get_languages
test/
runtests.jl # Full test suite using @testset
```

## Environment Variables

- `DEEPL_API_KEY` — required for all API calls and tests
- `DEEPL_API_URL` — optional override (defaults to `https://api.deepl.com/v2`)

## Code Conventions

- **Multiple dispatch:** define the same function for `String` and `Vector{String}` signatures
- **Pair syntax:** support `source => target` as a language pair argument (e.g., `"EN" => "DE"`)
- **Nullable types:** use `Optional{T} = Union{T, Nothing}`
- **Input validation:** guard clauses at the top of public functions; throw `ArgumentError` for invalid inputs
- **Errors:** `handle_api_error()` in `client.jl` checks HTTP status codes
- **Exports:** only public API functions are exported from the module (`translate_text`, `detect_language`, `get_languages`)
- **Imports:** use qualified imports (`using HTTP: HTTP`, `using JSON: JSON`)
- **Docstrings:** all public functions have Julia-style docstrings

## CI

GitHub Actions runs on push/PR to `main` with Julia 1.11. The `main` environment provides the `DEEPL_API_KEY` secret.

## Commit Style

Conventional commits: `feat:`, `fix:`, `chore:`, `test:`, `docs:`
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "DeepL"
uuid = "ee04fb13-afea-4bee-b967-185145acd83e"
authors = ["Pablo Valdunciel <pablo.valdunciel@docyet.com>"]
version = "0.2.0"
version = "0.3.0"

[deps]
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Expand Down
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
# DeepL SDK for Julia

DeepL SDK in Julia provides basic functionality for translating text strings. This package allows you to translate text from one language to another using the DeepL API.
DeepL SDK in Julia provides functionality for translating text strings, detecting languages and listing available languages. This package allows you to interact with the DeepL API from Julia.

## Setup
To use this package, you need to set the environment variable `DEEPL_API_KEY` with your DeepL API key.

## Available Functions

| Function | Description | Input | Output |
|---|---|---|---|
| `translate_text(text, source, target)` | Translate text specifying source and target languages | `String` or `Vector{String}` | `String` or `Vector{String}` |
| `translate_text(text, source => target)` | Translate text using Pair syntax | `String` or `Vector{String}` | `String` or `Vector{String}` |
| `translate_text(text, target)` | Translate text with auto-detected source language | `String` or `Vector{String}` | `String` or `Vector{String}` |
| `detect_language(text)` | Detect the language of the given text | `String` or `Vector{String}` | `String` or `Vector{String}` |
| `get_languages(type)` | List supported languages | `"source"` (default) or `"target"` | `Vector{Dict{String, Any}}` |
Comment thread
pabvald marked this conversation as resolved.

## Usage
First, import the package:
```julia
Expand Down Expand Up @@ -50,6 +60,41 @@ translate_text(texts, "EN" => "DE")
# "Ich bin sooo müde, Mann..."
```

### Detect Language
Detect the language of a single text:

```julia
detect_language("Guten Morgen, ich hätte gerne einen Tee")
# Output: "DE"

detect_language("Good morning, I would like a tea")
# Output: "EN"
```

Detect the language of multiple texts in a single API call:

```julia
detect_language(["Bonjour le monde", "Hola mundo", "Ciao mondo"])
# Output: ["FR", "ES", "IT"]
```

> **Note:** Since the DeepL API does not provide a dedicated language detection endpoint, `detect_language` uses the translate endpoint internally and extracts the detected source language from the response.

### List Available Languages
Retrieve the list of supported source languages:

```julia
get_languages()
# Output: [Dict("language" => "DE", "name" => "German"), Dict("language" => "EN", "name" => "English"), ...]
```

Retrieve the list of supported target languages (includes `supports_formality` field):

```julia
get_languages("target")
# Output: [Dict("language" => "DE", "name" => "German", "supports_formality" => true), ...]
```

## Resources

- [DeepL API documentation](https://developers.deepl.com/docs/getting-started/intro).
4 changes: 3 additions & 1 deletion src/DeepL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ const Optional{T} = Union{T, Nothing}
# ---------------
include("client.jl")
include("translate.jl")
export translate_text
include("detect.jl")
include("languages.jl")
export translate_text, detect_language, get_languages

"""
__init__()
Expand Down
34 changes: 27 additions & 7 deletions src/client.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Constants
# ---------
const HEADERS = Dict("Authorization" => "DeepL-Auth-Key " * DEEPL_API_KEY, "Content-Type" => "application/json")
const AUTH_HEADERS = Dict(
"Authorization" => "DeepL-Auth-Key " * DEEPL_API_KEY
)
const HEADERS = Dict(
"Authorization" => "DeepL-Auth-Key " * DEEPL_API_KEY,
"Content-Type" => "application/json"
)

"""
post_request(endpoint::String, body::Dict)
Expand All @@ -18,20 +24,34 @@ function post_request(endpoint::String, body::Dict)
return response
end

"""
get_request(endpoint::String; params::Dict=Dict())

Make a GET request to the DeepL API.
# Arguments
- `endpoint::String`: The endpoint to request.
- `params::Dict`: Optional query parameters.
# Returns
- `HTTP.Response`: The HTTP response from the API.
"""
function get_request(endpoint::String; params::Dict=Dict())
url = DEEPL_API_URL * endpoint
response = HTTP.get(url, AUTH_HEADERS; query=params)
return response
end

"""
handle_api_error(response::HTTP.Response)

Handle errors from the DeepL API response.
Check the DeepL API response for errors and throw an `ErrorException` if the request failed.

# Arguments
- `response::HTTP.Response`: The HTTP response from the API.

# Returns
- `String`: An error message if an error occurred, otherwise an empty string.
"""
function handle_api_error(response::HTTP.Response)
if response.status != 200
error_info = JSON.parse(String(response.body))
return "Error: " * get(error_info, "message", "Unknown error")
error("DeepL API error (HTTP $(response.status)): " * get(error_info, "message", "Unknown error"))
end
return ""
return nothing
end
50 changes: 50 additions & 0 deletions src/detect.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Public API
# ----------

"""
detect_language(text::AbstractString)

Detect the language of the given text using the DeepL API. The env variable `DEEPL_API_KEY` must be set.

Since the DeepL API does not provide a dedicated language detection endpoint, this function
uses the translate endpoint without specifying a source language and extracts the detected
language from the response.

# Arguments
- `text::AbstractString`: The text whose language should be detected.

# Returns
- `String`: The detected language code (e.g. "DE", "EN", "FR").
"""
function detect_language(text::AbstractString)
isempty(text) && throw(ArgumentError("text must not be empty"))

body = Dict("text" => [text], "target_lang" => "EN")

response = post_request("/translate", body)
handle_api_error(response)
result = JSON.parse(String(response.body))
return result["translations"][1]["detected_source_language"]
end

"""
detect_language(text::Vector{<:AbstractString})

Detect the language of each text in the given vector using the DeepL API. The env variable `DEEPL_API_KEY` must be set.

# Arguments
- `text::Vector{<:AbstractString}`: The texts whose languages should be detected.

# Returns
- `Vector{String}`: The detected language codes (e.g. ["DE", "EN", "FR"]).
"""
function detect_language(text::Vector{<:AbstractString})
isempty(text) && throw(ArgumentError("text must not be empty"))

body = Dict("text" => text, "target_lang" => "EN")

response = post_request("/translate", body)
handle_api_error(response)
result = JSON.parse(String(response.body))
return [t["detected_source_language"] for t in result["translations"]]
end
24 changes: 24 additions & 0 deletions src/languages.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Public API
# ----------

"""
get_languages(type::String="source")

Retrieve the list of languages supported by the DeepL API. The env variable `DEEPL_API_KEY` must be set.

# Arguments
- `type::String`: The type of languages to retrieve. Must be `"source"` or `"target"`. Defaults to `"source"`.

# Returns
- `Vector{Dict{String, Any}}`: A list of language objects, each containing:
- `"language"`: The language code (e.g. `"DE"`, `"EN"`, `"EN-GB"`).
- `"name"`: The human-readable language name (e.g. `"German"`, `"English"`).
- `"supports_formality"`: Whether formality options are available (target languages only).
"""
function get_languages(type::String="source")
type in ("source", "target") || throw(ArgumentError("type must be \"source\" or \"target\", got \"$type\""))

response = get_request("/languages"; params=Dict("type" => type))
handle_api_error(response)
return JSON.parse(String(response.body))
end
14 changes: 4 additions & 10 deletions src/translate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,7 @@ function translate_text(
!isnothing(source_lang) && (body["source_lang"] = source_lang)

response = post_request("/translate", body)
error_message = handle_api_error(response)
if !isempty(error_message)
return error_message
end
handle_api_error(response)
result = JSON.parse(String(response.body))
return result["translations"][1]["text"]
end
Expand Down Expand Up @@ -145,19 +142,16 @@ function translate_text(
(isempty(text) || source_lang == target_lang) && return text

# guard: check if the source and target languages are supported
!isnothing(source_lang) && !in(source_lang, SUPPORTED_SOURCE_LANGUAGES) &&
!isnothing(source_lang) && !in(source_lang, SUPPORTED_SOURCE_LANGUAGES) &&
throw(ArgumentError("source language '$source_lang' is not supported"))
!in(target_lang, SUPPORTED_TARGET_LANGUAGES) &&
!in(target_lang, SUPPORTED_TARGET_LANGUAGES) &&
throw(ArgumentError("target language '$target_lang' is not supported"))

body = Dict("text" => text, "target_lang" => target_lang)
!isnothing(source_lang) && (body["source_lang"] = source_lang)

response = post_request("/translate", body)
error_message = handle_api_error(response)
if !isempty(error_message)
return [error_message]
end
handle_api_error(response)
result = JSON.parse(String(response.body))
return [t["text"] for t in result["translations"]]
end
Expand Down
50 changes: 50 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,55 @@ using DeepL
@test translate_text(["Hallo", "Welt"], "DE" => "EN") == ["Hello", "World"]
@test translate_text(["Hallo", "Welt"], "ES") == ["Hola", "Mundo"]
end

@testset "invalid languages" begin
@test_throws ArgumentError translate_text("Hallo", "XX", "EN")
@test_throws ArgumentError translate_text("Hallo", "DE", "XX")
@test_throws ArgumentError translate_text(["Hallo"], "XX", "EN")
@test_throws ArgumentError translate_text(["Hallo"], "DE", "XX")
end
end

@testset "detect_language" begin
@testset "single text" begin
@test detect_language("Hallo Welt") == "DE"
@test detect_language("Hello world") == "EN"
@test detect_language("Bonjour le monde") == "FR"
end

@testset "multiple texts" begin
@test detect_language(["Hallo Welt", "Hello world"]) == ["DE", "EN"]
@test detect_language(["Bonjour madame", "Hola señorita", "Buongiorno, come stai oggi?"]) == ["FR", "ES", "IT"]
end

@testset "empty text" begin
@test_throws ArgumentError detect_language("")
@test_throws ArgumentError detect_language(String[])
end
end

@testset "get_languages" begin
@testset "source languages" begin
langs = get_languages()
@test langs isa Vector
@test length(langs) > 0
@test all(haskey(lang, "language") && haskey(lang, "name") for lang in langs)
# German should be in the source languages
@test any(lang["language"] == "DE" for lang in langs)
end

@testset "target languages" begin
langs = get_languages("target")
@test langs isa Vector
@test length(langs) > 0
@test all(haskey(lang, "language") && haskey(lang, "name") for lang in langs)
# Target languages include regional variants
@test any(lang["language"] == "EN-GB" for lang in langs)
@test all(haskey(lang, "supports_formality") for lang in langs)
end

@testset "invalid type" begin
@test_throws ArgumentError get_languages("invalid")
end
end
end