Common Words
Print-form snippets that write an amount of money, or a plain quantity, out in words, in Ukrainian, English or German.
The platform has no number-to-words filter on purpose: the languages and the currency names live in this module, so a fix or a new currency is a module update, not a platform release. A worked template is in common-words-demo.
| Amount, currency | Language | Result |
|---|---|---|
| 1234.56 UAH | uk |
одна тисяча двісті тридцять чотири гривні 56 копійок |
| 1234.56 USD | en |
one thousand two hundred thirty-four US dollars 56 cents |
| 1234.56 EUR | de |
eintausendzweihundertvierunddreißig Euro 56 Cent |
Use it in a print template
Set the inputs, then include the snippet. Both lines go into the template that prints the document (an invoice, a receipt, ...), not into this module:
{% assign words_amount = total %}
{% assign words_currency = currency_cd %}
{% assign words_lang = 'en' %}
{% include 'common-words.money' %}
| Input | Required | Meaning |
|---|---|---|
words_amount |
yes | The amount. A numeric field, not text: the server's Liquid engine does not turn a string into a number. A negative amount starts with «мінус» / «minus». |
words_currency |
yes | An ISO 4217 code, any case (UAH, usd). |
words_lang |
no | uk (default), en or de. Any other value prints [common-words: unknown language xx] instead of silently writing another language. |
words_cap |
no | true starts the text with a capital letter (One thousand ...). Liquid's own capitalize lowercases the rest of the text, so use this. |
The snippet prints the text and nothing else, so it can sit inside a paragraph or a table cell: <p>Total: {% include 'common-words.money' %}</p>.
Where the values come from
- Amount and currency are fields of the document you print. Use their column codes.
- Language is whatever your document knows: a fixed word (
'de'), a dropdown on the document, a setting. A dropdown prints as its label ("Deutsch"), not its code, so read the code from_raw:{% assign words_lang = _raw.lang %}. - A reference column (a currency picked from a list) prints as its display text. That is the code only if the referenced entity's
toStringis the code; otherwise keep the code in a plain column of the document.
One fixed language
When a form is always in one language, include that snippet directly and skip words_lang:
{% include 'common-words.money_uk' %}
money_uk, money_en and money_de take the same words_amount, words_currency and words_cap.
A quantity in words
For a count of things rather than money: «двадцять одна штука», «twenty-one pieces», «einundzwanzig Stück».
{% assign words_number = qty %}
{% assign words_unit = 'штука|штуки|штук' %}
{% assign words_gender = 'f' %}
{% assign words_lang = 'uk' %}
{% include 'common-words.quantity' %}
| Input | Required | Meaning |
|---|---|---|
words_number |
yes | The quantity, a numeric field. A fraction is rounded half up to a whole number; a negative one starts with «мінус» / «minus». |
words_unit |
no | The unit's forms joined by |: three for Ukrainian (1, 2–4, 5+), two for English and German (1, other). One form is used for every number. Without it only the number is written. |
words_gender |
no | m (default), f or n: «один / одна / одне», «ein / eine / ein». English ignores it. |
words_lang, words_cap |
no | As for money. |
| Quantity | Language | Result |
|---|---|---|
21, штука|штуки|штук, f |
uk |
двадцять одна штука |
2, кілограм|кілограми|кілограмів, m |
uk |
два кілограми |
21, piece|pieces |
en |
twenty-one pieces |
21, Stück |
de |
einundzwanzig Stück |
quantity_uk, quantity_en and quantity_de can be included directly, like the money ones. A thousand is always feminine in Ukrainian («дві тисячі»), whatever words_gender says.
What it does
- Minor units are written as two digits with their name («56 копійок», «56 cents», «56 Cent»). A currency with no minor unit (JPY) has none, and the amount is rounded half up to a whole number: 1234.56 JPY is «one thousand two hundred thirty-five yen».
- Rounding is half up to the minor unit. Amounts are money, so two decimals; a third decimal that is exactly a tie can round differently in print and in the preview.
- Zero is spelled («нуль гривень 00 копійок», «zero ...», «null ...»).
- Grammar follows each language: Ukrainian picks the singular, the 2–4 form or the plural by the last digits and the noun's gender («одна гривня», «дві гривні», «п'ять гривень», «дві тисячі»); English pluralises except for 1; German writes below a million as one word, uses «eine Million», and says «ein Euro» / «eine Hrywnja» for 1.
- Up to 999 trillion.
- A currency the module does not list prints the number and its code, minor units in digits: «three SEK 00».
Currencies
UAH, USD, EUR, GBP, PLN, CHF, CAD, AUD, JPY, RUB.
Add a currency
Each language file starts with a case on the currency code. Add a branch to each of print_templates/money_uk.html, money_en.html and money_de.html:
{%- when 'CZK' -%}
{%- assign w_unit = 'koruna|koruny|korun' -%}
{%- assign w_minor = 'halíř|haléře|haléřů' -%}
w_unit/w_minorare the noun forms joined by|. Ukrainian takes three (one / few / many: 1, 2–4, 5+), English and German two (one / other). A noun that never changes takes one form («євро», «Euro»).w_fem = true(Ukrainian) orw_gender = 'f'/'n'(German) when the unit is feminine / neuter: it decides «одна» / «eine» and «ein».w_dec = 0when the currency has no minor unit.- Leave
w_minorout when there is no minor-unit word to print.
Bump the module version in manifest.json and add a changelog/{version}.md. Tenants get the new text on the next module upgrade.
Add a language
- Copy
print_templates/money_en.html(two noun forms) ormoney_uk.html(three) tomoney_xx.htmland rewrite the number words, the group assembly and the currencycasefor that language. The skeleton (splitting the amount into whole and minor units, the sign, the loop over the thousands groups, the capital letter) stays; the grammar is what changes. - Do the same for
quantity_xx.html, fromquantity_en.htmlorquantity_uk.html. - Declare both in
ui/print_templates.jsonas asnippetwith"file": "money_xx.html"/"quantity_xx.html". - Add a
when 'xx'line toprint_templates/money.htmlandquantity.html. - Add cases for it to
clients/packages/data-ui/test/common-words.cases.json(lang,v,cur,expected) andcommon-words.quantity-cases.json(lang,v,unit,gender,expected) and run both test suites below. Check everyexpectedagainst the language's grammar by hand: a wrong value in the file is a wrong value in both engines.
Write snippets for both engines
A snippet runs in two Liquid engines: Scriban on the server (print) and LiquidJS in the browser (preview and print from a card). Output must be identical, so:
- No
floor/roundon big numbers. Scriban'sfloorreturns a 32-bit int. Split withmoduloinstead (see the top of anymoney_xx.html). - No number from
splitorslice. Scriban keeps it a string and"1000" | plus: 0concatenates. Branch withcase/if, or useforloopand(0..n)ranges. - No
{%- comment -%}with trim dashes. Scriban prints the comment. Use{% comment %}...{% endcomment -%}. - No parentheses in conditions; Liquid has none. Use nested
if. - Trim whitespace with
{%- ... -%}so the snippet prints only the text.
Tests
The same case list runs against both engines:
# server: Scriban
cd server && dotnet test tests/dForge.Api.Tests --filter "FullyQualifiedName~CommonWords"
# client: LiquidJS
cd clients/packages/data-ui && pnpm exec vitest run src/lib/print/common-words.test.ts
The server run also installs the module into a fresh tenant and checks that every stored snippet is the file shipped here.