Skip to content

Commit

Permalink
Add first working version of the library :)
Browse files Browse the repository at this point in the history
  • Loading branch information
andre-dasilva committed Jun 1, 2024
0 parents commit 024e833
Show file tree
Hide file tree
Showing 13 changed files with 449 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: test

on:
push:
branches:
- master
- main
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: "26.0.2"
gleam-version: "1.2.1"
rebar3-version: "3"
# elixir-version: "1.15.4"
- run: gleam deps download
- run: gleam test
- run: gleam format --check src test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Translator for gleam

[![Package Version](https://img.shields.io/hexpm/v/translator)](https://hex.pm/packages/translator)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/translator/)
[![Tests](https://github.com/andre-dasilva/translator/actions/workflows/test.yml/badge.svg)](https://github.com/andre-dasilva/translator/actions/workflows/test.yml)


Further documentation can be found at <https://hexdocs.pm/translator>.

## Getting Started

### Installation

```sh
gleam add translator
```

### Usage

Create a file for each locale in `src/`

```md
src/
└── locale/
└── de-CH/
└── lang.json
└── en-US/
└── lang.json
```

de-CH/lang.json

```json
{
"hello": {
"value": "Willkommen"
}
}
```

en-US/lang.json

```json
{
"hello": {
"value": "Welcome"
}
}
```

And use the translator like this

```gleam
import translator/translator
let assert Ok(translator) =
translator.new_translator("de-CH")
|> translator.with_directory("src/locale")
|> translator.from_json()
translator.get_key("hello")
// Willkommen
```

## Development

Fork the project and clone it

You can run the tests with:

```sh
gleam test
```
14 changes: 14 additions & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name = "translator"
version = "0.0.1"

description = "Translate gleam applications with locale files"
licences = ["MIT"]
repository = { type = "github", user = "andre-dasilva", repo = "translator" }

[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
simplifile = ">= 2.0.0 and < 3.0.0"
gleam_json = ">= 1.0.0 and < 2.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
17 changes: 17 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This file was generated by Gleam
# You typically do not need to edit this file

packages = [
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
{ name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
{ name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
{ name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
{ name = "simplifile", version = "2.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "95219227A43FCFE62C6E494F413A1D56FF953B68FE420698612E3D89A1EFE029" },
{ name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
]

[requirements]
gleam_json = { version = ">= 1.0.0 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
simplifile = { version = ">= 2.0.0 and < 3.0.0" }
11 changes: 11 additions & 0 deletions src/error.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import gleam/json
import simplifile

pub type TranslatorError {
DirectoryNotSet
DirectoryNotFound
KeyNotFound
LanguageFileNotFound
JsonError(json.DecodeError)
FileError(simplifile.FileError)
}
43 changes: 43 additions & 0 deletions src/header.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import gleam/float
import gleam/list
import gleam/option.{type Option}
import gleam/result
import gleam/string

/// Parses the accept langauge header according to:
/// https://www.rfc-editor.org/rfc/rfc9110#field.accept-language
///
/// e.g. fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
pub fn parse_accept_language_header(
raw_header: String,
) -> List(#(String, Float)) {
let quality_factor = ";q="

raw_header
|> string.replace(" ", "")
|> string.split(",")
|> list.filter(fn(lang) { !string.is_empty(lang) })
|> list.filter_map(fn(lang) {
case string.contains(lang, quality_factor) {
True -> string.split_once(lang, quality_factor)
False -> Ok(#(lang, "1.0"))
}
})
|> list.filter_map(fn(lang_with_priority) {
use priority <- result.try(float.parse(lang_with_priority.1))
Ok(#(lang_with_priority.0, priority))
})
|> list.sort(fn(a, b) { float.compare(a.1, b.1) })
|> list.reverse()
}

pub fn get_first_lang_from_accept_language_header(
raw_header: String,
) -> Option(String) {
let languages_with_priority = parse_accept_language_header(raw_header)

languages_with_priority
|> list.first()
|> option.from_result()
|> option.map(fn(lang_with_priority) { lang_with_priority.0 })
}
30 changes: 30 additions & 0 deletions src/json.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import error
import gleam/dict.{type Dict}
import gleam/dynamic
import gleam/json
import gleam/result
import gleam/string
import simplifile

const language_file_name = "lang.json"

pub fn read_file(
directory: String,
) -> Result(Dict(String, Dict(String, String)), error.TranslatorError) {
let file =
directory |> string.append("/") |> string.append(language_file_name)

use json_content <- result.try(
simplifile.read(file) |> result.map_error(error.FileError),
)

let decoder =
dynamic.dict(dynamic.string, dynamic.dict(dynamic.string, dynamic.string))

use translations <- result.try(result.map_error(
json.decode(from: json_content, using: decoder),
error.JsonError,
))

Ok(translations)
}
96 changes: 96 additions & 0 deletions src/translator.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import error
import gleam/dict.{type Dict}
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
import json
import simplifile

pub type Translator {
Translator(
language: String,
directory: Option(String),
translations: Option(Dict(String, Dict(String, String))),
)
}

const default_translations_directory = "src/translations"

pub fn new_translator(language: String) -> Translator {
Translator(
language: language,
directory: Some(default_translations_directory),
translations: None,
)
}

pub fn with_directory(translator: Translator, directory: String) -> Translator {
Translator(..translator, directory: Some(directory))
}

pub fn from_json(
translator: Translator,
) -> Result(Translator, error.TranslatorError) {
use directory <- result.try(option.to_result(
translator.directory,
error.DirectoryNotSet,
))

let directory_with_language =
directory |> string.append("/") |> string.append(translator.language)

use is_directory <- result.try(
simplifile.is_directory(directory_with_language)
|> result.map_error(error.FileError),
)

use translations <- result.try(case is_directory {
True -> Ok(json.read_file(directory_with_language))
False -> Error(error.DirectoryNotFound)
})

use translations <- result.try(translations)

Ok(Translator(..translator, translations: Some(translations)))
}

pub fn get_key(
translator: Translator,
key: String,
) -> Result(String, error.TranslatorError) {
let translations = translator.translations |> option.unwrap(dict.new())

use translation <- result.try(
dict.get(translations, key) |> result.replace_error(error.KeyNotFound),
)

dict.get(translation, "value") |> result.replace_error(error.KeyNotFound)
}

fn rec_replace_args(value: String, args: List(#(String, String))) -> String {
case args {
[head, ..tail] -> {
let #(param, param_value) = head

let updated_translation =
string.replace(
value,
each: "{" <> string.uppercase(param) <> "}",
with: param_value,
)

rec_replace_args(updated_translation, tail)
}
_ -> value
}
}

pub fn get_key_with_args(
translator: Translator,
key: String,
args: List(#(String, String)),
) -> Result(String, error.TranslatorError) {
use translation <- result.try(get_key(translator, key))

Ok(rec_replace_args(translation, args))
}
47 changes: 47 additions & 0 deletions test/header_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import gleam/list
import gleam/option.{None, Some}
import gleeunit/should
import header

pub fn parse_accept_language_header_test() {
let tests = [
#("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", [
#("fr-CH", 1.0),
#("fr", 0.9),
#("en", 0.8),
#("de", 0.7),
#("*", 0.5),
]),
#("fr-CH, de-CH;q=0.2, en;q=0.5, it;q=0.3", [
#("fr-CH", 1.0),
#("en", 0.5),
#("it", 0.3),
#("de-CH", 0.2),
]),
#("fr-CH, de-CH;q=A, en;q=0.2", [#("fr-CH", 1.0), #("en", 0.2)]),
#("", []),
]

tests
|> list.map(fn(t) {
let #(input, expected) = t

header.parse_accept_language_header(input)
|> should.equal(expected)
})
}

pub fn get_first_lang_from_accept_language_header_test() {
let tests = [
#("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", Some("fr-CH")),
#("", None),
]

tests
|> list.map(fn(t) {
let #(input, expected) = t

header.get_first_lang_from_accept_language_header(input)
|> should.equal(expected)
})
}
14 changes: 14 additions & 0 deletions test/locale/de-CH/lang.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hello": {
"value": "Hallo"
},
"welcome": {
"value": "Willkommen"
},
"this_is_great": {
"value": "Das ist fantastisch"
},
"amount_planets": {
"value": "Es gibt {AMOUNT} Planeten. Aber es waren mal {AMOUNT_BEFORE}."
}
}
14 changes: 14 additions & 0 deletions test/locale/en-US/lang.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hello": {
"value": "Hello"
},
"welcome": {
"value": "Welcome"
},
"this_is_great": {
"value": "This is great"
},
"amount_planets": {
"value": "There are {AMOUNT} planets"
}
}
Loading

0 comments on commit 024e833

Please sign in to comment.