From 1201ccfd2954832279df09374f975dd6eb35e6da Mon Sep 17 00:00:00 2001 From: Pablo Valdunciel Date: Thu, 19 Feb 2026 09:38:04 +0100 Subject: [PATCH 1/2] feat: add detect_language and get_languages functions - Add `detect_language` for single and multiple texts using the translate endpoint - Add `get_languages` to list supported source/target languages via GET request - Add `get_request` HTTP helper and `AUTH_HEADERS` constant - Update README with documentation and usage examples - Add CLAUDE.md with project conventions - Add tests for new functions - Bump version to 0.3.0 --- CHANGELOG.md | 12 +++++++++++ CLAUDE.md | 52 ++++++++++++++++++++++++++++++++++++++++++++ Project.toml | 2 +- README.md | 47 +++++++++++++++++++++++++++++++++++++++- src/DeepL.jl | 4 +++- src/client.jl | 24 ++++++++++++++++++++- src/detect.jl | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ src/languages.jl | 27 +++++++++++++++++++++++ test/runtests.jl | 43 +++++++++++++++++++++++++++++++++++++ 9 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/detect.jl create mode 100644 src/languages.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6f901..a180116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e645786 --- /dev/null +++ b/CLAUDE.md @@ -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:` diff --git a/Project.toml b/Project.toml index ea119fc..d58a20c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DeepL" uuid = "ee04fb13-afea-4bee-b967-185145acd83e" authors = ["Pablo Valdunciel "] -version = "0.2.0" +version = "0.3.0" [deps] HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" diff --git a/README.md b/README.md index ef0fd05..2d71c0c 100644 --- a/README.md +++ b/README.md @@ -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}}` | + ## Usage First, import the package: ```julia @@ -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). diff --git a/src/DeepL.jl b/src/DeepL.jl index 575c3d3..d07a6ae 100644 --- a/src/DeepL.jl +++ b/src/DeepL.jl @@ -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__() diff --git a/src/client.jl b/src/client.jl index 1ede570..69ad823 100644 --- a/src/client.jl +++ b/src/client.jl @@ -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) @@ -18,6 +24,22 @@ 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) diff --git a/src/detect.jl b/src/detect.jl new file mode 100644 index 0000000..203589e --- /dev/null +++ b/src/detect.jl @@ -0,0 +1,56 @@ +# 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) + error_message = handle_api_error(response) + if !isempty(error_message) + return error_message + end + 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) + error_message = handle_api_error(response) + if !isempty(error_message) + return [error_message] + end + result = JSON.parse(String(response.body)) + return [t["detected_source_language"] for t in result["translations"]] +end diff --git a/src/languages.jl b/src/languages.jl new file mode 100644 index 0000000..35c7b99 --- /dev/null +++ b/src/languages.jl @@ -0,0 +1,27 @@ +# 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)) + error_message = handle_api_error(response) + if !isempty(error_message) + return error_message + end + return JSON.parse(String(response.body)) +end diff --git a/test/runtests.jl b/test/runtests.jl index 941ca23..1ca7c54 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,4 +16,47 @@ using DeepL @test translate_text(["Hallo", "Welt"], "ES") == ["Hola", "Mundo"] 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 From a8d361419497c4d3a179dfe6a35032490d1cf182 Mon Sep 17 00:00:00 2001 From: Pablo Valdunciel Date: Thu, 19 Feb 2026 10:00:58 +0100 Subject: [PATCH 2/2] refactor: error handling --- src/client.jl | 10 ++++------ src/detect.jl | 10 ++-------- src/languages.jl | 5 +---- src/translate.jl | 14 ++++---------- test/runtests.jl | 7 +++++++ 5 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/client.jl b/src/client.jl index 69ad823..5d9d1ad 100644 --- a/src/client.jl +++ b/src/client.jl @@ -43,17 +43,15 @@ 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 diff --git a/src/detect.jl b/src/detect.jl index 203589e..980bc2e 100644 --- a/src/detect.jl +++ b/src/detect.jl @@ -22,10 +22,7 @@ function detect_language(text::AbstractString) body = Dict("text" => [text], "target_lang" => "EN") 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]["detected_source_language"] end @@ -47,10 +44,7 @@ function detect_language(text::Vector{<:AbstractString}) body = Dict("text" => text, "target_lang" => "EN") 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["detected_source_language"] for t in result["translations"]] end diff --git a/src/languages.jl b/src/languages.jl index 35c7b99..0315241 100644 --- a/src/languages.jl +++ b/src/languages.jl @@ -19,9 +19,6 @@ 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)) - error_message = handle_api_error(response) - if !isempty(error_message) - return error_message - end + handle_api_error(response) return JSON.parse(String(response.body)) end diff --git a/src/translate.jl b/src/translate.jl index 7f8fa3a..9736cfa 100644 --- a/src/translate.jl +++ b/src/translate.jl @@ -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 @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 1ca7c54..c603174 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,6 +15,13 @@ 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