Skip to content

Commit a6d752e

Browse files
committed
Introduce Glossary
0 parents  commit a6d752e

File tree

14 files changed

+748
-0
lines changed

14 files changed

+748
-0
lines changed

.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

.github/workflows/glossary.yml

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
name: Glossary CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
MIX_ENV: test
11+
12+
jobs:
13+
deps:
14+
name: Dependencies
15+
runs-on: ubuntu-latest
16+
strategy:
17+
matrix:
18+
elixir: [1.17.3, 1.18.4]
19+
otp: [26.2.5.12, 27.3.4]
20+
exclude:
21+
- elixir: 1.17.3
22+
otp: 28.0
23+
steps:
24+
- name: Cancel Previous Runs
25+
uses: styfle/cancel-workflow-action@0.12.1
26+
with:
27+
access_token: ${{ github.token }}
28+
29+
- name: Checkout
30+
uses: actions/checkout@v4.2.2
31+
with:
32+
fetch-depth: 0
33+
34+
- name: Setup
35+
uses: erlef/setup-beam@v1.19
36+
with:
37+
elixir-version: ${{ matrix.elixir }}
38+
otp-version: ${{ matrix.otp }}
39+
40+
- name: Retrieve Cached Dependencies
41+
uses: actions/cache@v4.2.2
42+
id: mix-cache
43+
with:
44+
path: |
45+
deps
46+
_build
47+
priv/plts
48+
key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}-2
49+
50+
- name: Install Dependencies
51+
if: steps.mix-cache.outputs.cache-hit != 'true'
52+
run: |
53+
mkdir -p priv/plts
54+
mix local.rebar --force
55+
mix local.hex --force
56+
mix deps.get
57+
mix deps.compile
58+
mix dialyzer --plt
59+
60+
static_code_analysis:
61+
name: Static Code Analysis
62+
needs: deps
63+
runs-on: ubuntu-latest
64+
strategy:
65+
matrix:
66+
elixir: [1.17.3, 1.18.4]
67+
otp: [26.2.5.12, 27.3.4]
68+
exclude:
69+
- elixir: 1.17.3
70+
otp: 28.0
71+
steps:
72+
- name: Cancel Previous Runs
73+
uses: styfle/cancel-workflow-action@0.12.1
74+
with:
75+
access_token: ${{ github.token }}
76+
77+
- name: Checkout
78+
uses: actions/checkout@v4.2.2
79+
with:
80+
fetch-depth: 0
81+
82+
- name: Setup
83+
uses: erlef/setup-beam@v1.19
84+
with:
85+
elixir-version: ${{ matrix.elixir }}
86+
otp-version: ${{ matrix.otp }}
87+
88+
- name: Retrieve Cached Dependencies
89+
uses: actions/cache@v4.2.2
90+
id: mix-cache
91+
with:
92+
path: |
93+
deps
94+
_build
95+
priv/plts
96+
key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}-2
97+
98+
- name: Check Code Format
99+
run: |
100+
mix deps.get
101+
mix format --check-formatted
102+
103+
- name: Run Credo
104+
run: mix credo --strict
105+
106+
- name: Run Dialyzer
107+
run: mix dialyzer --no-check --halt-exit-status
108+
109+
unit_tests:
110+
name: Unit Tests
111+
needs: deps
112+
runs-on: ubuntu-latest
113+
services:
114+
postgres:
115+
image: postgres
116+
env:
117+
POSTGRES_DB: umwelt_test
118+
# match PGPASSWORD for elixir image above
119+
POSTGRES_PASSWORD: postgres
120+
# match PGUSER for elixir image above
121+
POSTGRES_USER: postgres
122+
# Set health checks to wait until postgres has started
123+
options: >-
124+
--health-cmd pg_isready
125+
--health-interval 10s
126+
--health-timeout 5s
127+
--health-retries 5
128+
ports:
129+
# Maps tcp port 5432 on service container to the host
130+
- 5432:5432
131+
strategy:
132+
fail-fast: false
133+
matrix:
134+
elixir: [1.17.3, 1.18.4]
135+
otp: [26.2.5.12, 27.3.4]
136+
exclude:
137+
- elixir: 1.17.3
138+
otp: 28.0
139+
steps:
140+
- name: Cancel Previous Runs
141+
uses: styfle/cancel-workflow-action@0.12.1
142+
with:
143+
access_token: ${{ github.token }}
144+
145+
- name: Checkout
146+
uses: actions/checkout@v4.2.2
147+
with:
148+
fetch-depth: 0
149+
150+
- name: Setup
151+
uses: erlef/setup-beam@v1.19
152+
with:
153+
elixir-version: ${{ matrix.elixir }}
154+
otp-version: ${{ matrix.otp }}
155+
156+
- name: Retrieve Cached Dependencies
157+
uses: actions/cache@v4.2.2
158+
id: mix-cache
159+
with:
160+
path: |
161+
deps
162+
_build
163+
priv/plts
164+
key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}-2
165+
166+
- name: Run test
167+
env:
168+
# match PGPASSWORD for elixir image above
169+
POSTGRES_PASSWORD: postgres
170+
# match PGUSER for elixir image above
171+
POSTGRES_USER: postgres
172+
run: mix test --trace --cover --slowest 10

.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
glossary-*.tar
24+
25+
# Temporary files, for example, from tests.
26+
/tmp/

.tool-versions

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
elixir 1.18.4
2+
erlang 28.0

README.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# Glossary
2+
3+
**Minimalistic semantic translation system for Elixir apps.**
4+
5+
Glossary is a lightweight and expressive alternative to gettext for modern Elixir applications — especially Phoenix LiveView.
6+
It embraces semantic lexemes, YAML lexicons, and compile-time localization with a simple and explicit API.
7+
8+
Each YAML file acts as a lexicon — a mapping of semantic keys (lexemes) to localized values (expressions) for a given language.
9+
All lexicons are compiled into the module at build time, enabling fast and predictable lookups at runtime.
10+
11+
---
12+
13+
## 🧱 Concept
14+
15+
A **lexeme** is a minimal semantic unit — a key like "game.won".
16+
An **expression** is its localized realization — a string like "You won!".
17+
A **lexicon** is a YAML file that maps lexemes to expressions for a specific language.
18+
19+
Together, lexicons form a **glossary** — a complete set of localized meanings.
20+
21+
---
22+
23+
## ✨ Features
24+
25+
- 🧠 **Semantic keys** — Use lexemes like `"game.score"`, not literal strings.
26+
- 🔄 **Runtime interpolation** — Simple bindings with `{{key}}` syntax.
27+
-**Live reloadable** — Load translations dynamically, perfect for LiveView.
28+
- 📄 **YAML-first** — Intuitive, version-friendly format.
29+
- 🧪 **No hidden magic** — Only explicit macros for setup, no DSLs, no runtime surprises.
30+
31+
---
32+
33+
## Lexicons (YAML)
34+
35+
Each YAML file represents a **lexicon** — a set of localized expressions.
36+
37+
Lexicons are merged into a single lookup table keyed by `"language.lexeme"`.
38+
39+
Example structure of lexicon:
40+
41+
```yaml
42+
# game.en.yml
43+
game:
44+
won: "You won!"
45+
lost: "Game over."
46+
score: "Your score: {{score}}"
47+
48+
# game.ru.yml:
49+
game:
50+
won: "Вы победили!"
51+
lost: "Игра окончена."
52+
score: "Ваш счёт: {{score}}"
53+
54+
# user.en.yml:
55+
user:
56+
greeting: "Hello, {{name}}!"
57+
58+
# user.ru.yml:
59+
user:
60+
greeting: "Привет, {{name}}!"
61+
```
62+
63+
---
64+
65+
## 🚀 Quick Start
66+
67+
### 1. Add to your project
68+
69+
[available in Hex](https://hex.pm/packages/glossary), the package can be installed
70+
by adding `glossary` to your list of dependencies in `mix.exs`:
71+
72+
```elixir
73+
# mix.exs
74+
def deps do
75+
[
76+
{:glossary, "~> 0.1"}
77+
]
78+
end
79+
```
80+
81+
### 2. Define a module with glossary, specify a lexicons list
82+
83+
```elixir
84+
# my_app_web/live/game/show.ex
85+
defmodule MyAppWeb.Live.Game.Show do
86+
use Glossary, ["game", "../users/user", "../common"]
87+
end
88+
```
89+
90+
This loads:
91+
92+
- `my_app_web/live/common.en.yml`, `my_app_web/live/common.ru.yml`
93+
- `my_app_web/live/game/game.en.yml`, `my_app_web/live/game/game.ru.yml`
94+
- `my_app_web/live/users/user.en.yml`, `my_app_web/live/users/user.ru.yml`
95+
96+
### 3. Use in LiveView templates by lexeme
97+
98+
```elixir
99+
<%= MyAppWeb.Live.Game.Show.t("game.score", @locale, score: 42) %>
100+
```
101+
102+
### 4. You’ll see the full key on the page, e.g., `en.game.score`
103+
And a warning in your logs:
104+
105+
```text
106+
[warning] [Glossary] Missing: en.game.score
107+
```
108+
109+
### 5. Add the translation to your YAML lexicon
110+
111+
```yaml
112+
# game.en.yml:
113+
game:
114+
score: "Your score: {{score}}"
115+
```
116+
117+
### 6. Reload the page — warning disappears, and translated text is shown.
118+
119+
### 7. Repeat until all template keys are covered and logs are clean.
120+
121+
### 8. Only after the primary language is complete, translate the rest by following the structure.
122+
123+
---
124+
125+
## 💡 API
126+
127+
```elixir
128+
t(lexeme, locale)
129+
t(lexeme, locale, bindings)
130+
```
131+
132+
- Falls back to `lexeme` if no translation is found.
133+
- Interpolates placeholders like `{{score}}` with values from `bindings`.
134+
135+
---
136+
137+
## 📚 Best Practices
138+
139+
- ✅ Use **semantic keys**: `"user.greeting"` > `"welcome_text_1"`
140+
- 📁 Group by domain: `user`, `game`, `import`, etc.
141+
- 🧩 Prefer flat 2-level keys: `domain.key`
142+
- 🔑 Avoid file-based logic — only lexemes and language matter
143+
- 🪄 Use `{{key}}` placeholders for dynamic values
144+
145+
---
146+
147+
## 🏛️ Philosophy
148+
149+
Glossary was built for **dynamic apps** — like those using Phoenix LiveView — where UI, state, and translations often evolve together.
150+
151+
### 🔍 Comparison with Gettext
152+
Glossary is built for interactive, reactive, hot-reloaded systems.
153+
Gettext was designed for monolithic, statically compiled apps in the 1990s.
154+
Phoenix LiveView is dynamic, reactive, and often developer-translated.
155+
Glossary brings translation into the runtime flow of development.
156+
157+
| Feature | Glossary | Gettext |
158+
|----------------------|---------------------|---------------------|
159+
| ✅ Semantic keys | Yes (`"home.title"`) | No (uses msgid) |
160+
| ✏️ YAML format | Yes | No (.po files) |
161+
| ♻️ Live reload | Easy | Needs recompilation |
162+
| 📦 Runtime API | Simple `t/2`, `t/3` | Macro-based |
163+
| 🧪 Dev experience | Transparent | Magic/macros |
164+
165+
---
166+
167+
### Why move beyond gettext?
168+
169+
- You want **declarative keys** and **semantic structure**
170+
- You want to **edit translations live**
171+
- You don’t want to manage `.po` files or run compilers
172+
- You want your **UI and language logic to stay in sync**
173+
174+
---
175+
176+
## 🧠 Acknowledgments
177+
178+
Inspired by the real-world needs of building modern Phoenix LiveView apps with:
179+
180+
- ✨ Declarative UIs
181+
- 🔁 Dynamic state
182+
- 🛠️ Developer-driven i18n
183+
184+
---
185+
186+
## 📬 Feedback
187+
188+
Glossary is small, hackable, and stable — and we're open to ideas.
189+
Raise an issue, suggest a feature, or just use it and tell us how it goes.
190+
191+
> Let your translations be as clean as your code.

0 commit comments

Comments
 (0)