Skip to content

Commit 48d0283

Browse files
Merge pull request #3757 from opral/add-unit-tests
add ordinal docs
2 parents 64982e4 + 80fbb1e commit 48d0283

File tree

5 files changed

+228
-3
lines changed

5 files changed

+228
-3
lines changed

inlang/packages/paraglide/paraglide-js/docs/basics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ To add a new locale, add it to the `locales` array in `<project0name>.inlang/set
2121
## Adding and editing messages
2222

2323
<doc-callout type="info">
24-
This section assumes you use the inlang message format plugin that is setup by default in Paraglide JS.
24+
These examples use the inlang message format plugin that ships by default, but Paraglide works with any format plugin that produces the expected message files. Swap the plugin in `project.inlang/settings.json` if you prefer a different storage format—see the <a href="https://inlang.com/c/plugins">plugin directory</a>.
2525
</doc-callout>
2626

2727
Messages are stored in `messages/{locale}.json` as key-value pairs. You can add parameters with curly braces.

inlang/packages/paraglide/paraglide-js/docs/variants.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ imports:
66

77
# Variants
88

9-
Variants enable pluralization, gendering, A/B testing, and more. They are a powerful feature of inlang that allows you to create different versions of a message based on conditions.
9+
Variants enable pluralization, gendering, A/B testing, and more. They are a powerful feature of inlang that allows you to create different versions of a message based on conditions.
1010

1111
## Matching
1212

@@ -58,4 +58,30 @@ You can define a variable in your message and then use it in the selector. Parag
5858
},
5959
}]
6060
}
61-
```
61+
```
62+
63+
### Ordinal pluralization (1st, 2nd, 3rd…)
64+
65+
`plural` forwards its options to `Intl.PluralRules`, so you can request ordinal categories by passing `type=ordinal`.
66+
67+
```json
68+
{
69+
"finished_readout": [{
70+
"declarations": [
71+
"input placeNumber",
72+
"local ordinalCategory = placeNumber: plural type=ordinal"
73+
],
74+
"selectors": ["ordinalCategory"],
75+
"match": {
76+
"ordinalCategory=one": "You finished in {placeNumber}st place",
77+
"ordinalCategory=two": "You finished in {placeNumber}nd place",
78+
"ordinalCategory=few": "You finished in {placeNumber}rd place",
79+
"ordinalCategory=*": "You finished in {placeNumber}th place"
80+
}
81+
}]
82+
}
83+
```
84+
85+
<doc-callout type="tip">
86+
Ordinal category names (`one`, `two`, `few`, `other`, etc.) follow <code>Intl.PluralRules</code> for the active locale.
87+
</doc-callout>

inlang/packages/paraglide/paraglide-js/src/compiler/compile-message.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,98 @@ test("compiles messages that use plural()", async () => {
237237
expect(plural_test({ count: undefined })).toBe("There are many cats.");
238238
});
239239

240+
test("compiles messages that use plural() with ordinal type", async () => {
241+
const declarations: Declaration[] = [
242+
{ type: "input-variable", name: "count" },
243+
{
244+
type: "local-variable",
245+
name: "countOrdinal",
246+
value: {
247+
arg: { type: "variable-reference", name: "count" },
248+
annotation: {
249+
type: "function-reference",
250+
name: "plural",
251+
options: [
252+
{ name: "type", value: { type: "literal", value: "ordinal" } },
253+
],
254+
},
255+
type: "expression",
256+
},
257+
},
258+
];
259+
const message: Message = {
260+
locale: "en",
261+
bundleId: "ordinal_test",
262+
id: "message_id",
263+
selectors: [{ type: "variable-reference", name: "countOrdinal" }],
264+
};
265+
const variants: Variant[] = [
266+
{
267+
id: "1",
268+
messageId: "message_id",
269+
matches: [{ type: "literal-match", value: "one", key: "countOrdinal" }],
270+
pattern: [
271+
{
272+
type: "expression",
273+
arg: { type: "variable-reference", name: "count" },
274+
},
275+
{ type: "text", value: "st place" },
276+
],
277+
},
278+
{
279+
id: "2",
280+
messageId: "message_id",
281+
matches: [{ type: "literal-match", value: "two", key: "countOrdinal" }],
282+
pattern: [
283+
{
284+
type: "expression",
285+
arg: { type: "variable-reference", name: "count" },
286+
},
287+
{ type: "text", value: "nd place" },
288+
],
289+
},
290+
{
291+
id: "3",
292+
messageId: "message_id",
293+
matches: [{ type: "literal-match", value: "few", key: "countOrdinal" }],
294+
pattern: [
295+
{
296+
type: "expression",
297+
arg: { type: "variable-reference", name: "count" },
298+
},
299+
{ type: "text", value: "rd place" },
300+
],
301+
},
302+
{
303+
id: "4",
304+
messageId: "message_id",
305+
matches: [{ type: "literal-match", value: "other", key: "countOrdinal" }],
306+
pattern: [
307+
{
308+
type: "expression",
309+
arg: { type: "variable-reference", name: "count" },
310+
},
311+
{ type: "text", value: "th place" },
312+
],
313+
},
314+
];
315+
316+
const compiled = compileMessage(declarations, message, variants);
317+
318+
const { ordinal_test } = await import(
319+
"data:text/javascript;base64," +
320+
btoa(createRegistry()) +
321+
btoa(
322+
"export const ordinal_test = " + compiled.code.replace("registry.", "")
323+
)
324+
);
325+
326+
expect(ordinal_test({ count: 1 })).toBe("1st place");
327+
expect(ordinal_test({ count: 2 })).toBe("2nd place");
328+
expect(ordinal_test({ count: 3 })).toBe("3rd place");
329+
expect(ordinal_test({ count: 4 })).toBe("4th place");
330+
});
331+
240332
test("compiles messages that use datetime()", async () => {
241333
const createMessage = async (locale: string) => {
242334
const declarations: Declaration[] = [

inlang/packages/plugins/inlang-message-format/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ imports:
88
The Inlang Message Format is a simple storage plugin for the Inlang ecosystem. It allows you to store simple
99
messages in a JSON file per language.
1010

11+
The syntax is inspired by the upcoming [MessageFormat 2.0](https://messageformat.unicode.org/) draft to keep migration friction low as the standard matures.
12+
1113
The message files contain key-value pairs of the message ID and the translation. You can add variables in your message by using curly braces.
1214

1315
```json
@@ -184,6 +186,34 @@ The message below will match the following conditions:
184186
}
185187
```
186188

189+
#### Ordinal pluralization (1st, 2nd, 3rd…)
190+
191+
`plural` forwards its options to `Intl.PluralRules`, so you can request ordinal categories by passing `type=ordinal` in your declaration.
192+
193+
```json
194+
{
195+
"finished_readout": [
196+
{
197+
"declarations": [
198+
"input placeNumber",
199+
"local ordinalCategory = placeNumber: plural type=ordinal"
200+
],
201+
"selectors": ["ordinalCategory"],
202+
"match": {
203+
"ordinalCategory=one": "You finished in {placeNumber}st place",
204+
"ordinalCategory=two": "You finished in {placeNumber}nd place",
205+
"ordinalCategory=few": "You finished in {placeNumber}rd place",
206+
"ordinalCategory=*": "You finished in {placeNumber}th place"
207+
}
208+
}
209+
]
210+
}
211+
```
212+
213+
<doc-callout type="tip">
214+
Ordinal category names (`one`, `two`, `few`, `other`, etc.) follow <code>Intl.PluralRules</code> for the active locale.
215+
</doc-callout>
216+
187217
Pluralization is also supported. You can define a variable in your message and then use it in the selector.
188218

189219
| Inputs | Condition | Message |

inlang/packages/plugins/inlang-message-format/src/import-export/roundtrip.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,83 @@ test("variants with a plural function are parsed correctly", async () => {
265265
);
266266
});
267267

268+
test("ordinal plural options survive import/export", async () => {
269+
const imported = await runImportFiles({
270+
finished_readout: [
271+
{
272+
declarations: [
273+
"input placeNumber",
274+
"local ordinalCategory = placeNumber: plural type=ordinal",
275+
],
276+
selectors: ["ordinalCategory"],
277+
match: {
278+
"ordinalCategory=one": "You finished in {placeNumber}st place",
279+
"ordinalCategory=two": "You finished in {placeNumber}nd place",
280+
"ordinalCategory=few": "You finished in {placeNumber}rd place",
281+
"ordinalCategory=*": "You finished in {placeNumber}th place",
282+
},
283+
},
284+
],
285+
});
286+
287+
expect(await runExportFilesParsed(imported)).toMatchObject({
288+
finished_readout: [
289+
{
290+
declarations: [
291+
"input placeNumber",
292+
"local ordinalCategory = placeNumber: plural type=ordinal",
293+
],
294+
selectors: ["ordinalCategory"],
295+
match: {
296+
"ordinalCategory=one": "You finished in {placeNumber}st place",
297+
"ordinalCategory=two": "You finished in {placeNumber}nd place",
298+
"ordinalCategory=few": "You finished in {placeNumber}rd place",
299+
"ordinalCategory=*": "You finished in {placeNumber}th place",
300+
},
301+
},
302+
],
303+
});
304+
305+
expect(imported.bundles[0]?.declarations).toContainEqual({
306+
type: "local-variable",
307+
name: "ordinalCategory",
308+
value: {
309+
type: "expression",
310+
arg: { type: "variable-reference", name: "placeNumber" },
311+
annotation: {
312+
type: "function-reference",
313+
name: "plural",
314+
options: [
315+
{ name: "type", value: { type: "literal", value: "ordinal" } },
316+
],
317+
},
318+
},
319+
});
320+
321+
expect(imported.messages[0]?.selectors).toContainEqual({
322+
type: "variable-reference",
323+
name: "ordinalCategory",
324+
});
325+
326+
const catchallVariant = imported.variants.find((variant) =>
327+
variant.matches?.some(
328+
(match) =>
329+
match.type === "catchall-match" && match.key === "ordinalCategory"
330+
)
331+
);
332+
333+
expect(catchallVariant).toMatchObject({
334+
pattern: [
335+
{ type: "text", value: "You finished in " },
336+
{
337+
type: "expression",
338+
arg: { type: "variable-reference", name: "placeNumber" },
339+
},
340+
{ type: "text", value: "th place" },
341+
],
342+
});
343+
});
344+
268345
test("doesn't use string as value if declarations, selectors, or multiple matches exist ", async () => {
269346
const imported = await runImportFiles({
270347
some_happy_cat: "Today is {date}.",

0 commit comments

Comments
 (0)