Skip to content

Commit

Permalink
Provide context/comment API without AA.
Browse files Browse the repository at this point in the history
The requirement that context and comments to the translator be
supplied as an associate array literal was a bit silly. It is still supported,
but not documented.
  • Loading branch information
veelo committed Aug 27, 2023
1 parent 42e3ffd commit dd94820
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 74 deletions.
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ Optionally, two kinds of attributes can be passed to `tr`, in the form of an ass

Sometimes a sentence can be interpreted to mean different things, and then it is important to be able to clarify things for the translator. Here is an example of how to do this:
```d
auto name = tr!("Walter Bright", [Tr.note: "Proper name. Phonetically: ˈwɔltər braɪt"]);
auto name = tr!("Walter Bright", Comment("Proper name. Phonetically: ˈwɔltər braɪt"));
```

The GNU `gettext` manual has a section [about the translation of proper names](https://www.gnu.org/software/gettext/manual/html_node/Names.html).
Expand All @@ -193,23 +193,23 @@ The GNU `gettext` manual has a section [about the translation of proper names](h

Multiple occurrences of the same sentence are combined into one translation by default. In some cases, that may not work well. Some language, for example, may need to translate identical menu items in different menus differently. These can be disambiguated by adding a context like so:
```d
auto labelOpenFile = tr!("Open", [Tr.context: "Menu|File"]);
auto labelOpenPrinter = tr!("Open", [Tr.context: "Menu|File|Printer"]);
auto labelOpenFile = tr!("Open", Context("Menu|File"));
auto labelOpenPrinter = tr!("Open", Context("Menu|File|Printer"));
```

Notes and comments can be combined in any order:
Notes and comments can be combined:
```d
auto message1 = tr!("Review the draft.", [Tr.context: "document"]);
auto message2 = tr!("Review the draft.", [Tr.context: "nautical",
Tr.note: `Nautical term! "Draft" = how deep the bottom` ~
`of the ship is below the water level.`]);
auto message1 = tr!("Review the draft.", Context("document"));
auto message2 = tr!("Review the draft.", Context("nautical"),
Comment(`Nautical term! "Draft" = how deep the bottom` ~
`of the ship is below the water level.`));
```

They work on plural forms too:
```d
writeln(tr!("One license.", "%d licenses.", [Tr.context: "software",
Tr.note: "Notice to translator."])(n));
writeln(tr!("One license.", "%d licenses.", [Tr.context: "driver's"])(n));
writeln(tr!("One license.", "%d licenses.", Context("software"),
Comment("Notice to translator."))(n));
writeln(tr!("One license.", "%d licenses.", Context("driver's"))(n));
```

## Selecting a translation
Expand Down
141 changes: 109 additions & 32 deletions source/gettext.d
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ translated differently. This can be accomplished by disambiguating the string wi
context argument. It is also possible to attach a comment that will be seen by
the translator:
---
auto message1 = tr!("Review the draft.", [Tr.context: "document"]);
auto message2 = tr!("Review the draft.", [Tr.context: "nautical",
Tr.note: `Nautical term! "Draft" = how deep the bottom` ~
`of the ship is below the water level.`]);
auto message1 = tr!("Review the draft.", Context("document"));
auto message2 = tr!("Review the draft.", Context("nautical"),
Comment(`Nautical term! "Draft" = how deep the bottom` ~
`of the ship is below the water level.`));
---
If you'd rather use an underscore to mark translatable strings,
Expand All @@ -65,10 +65,30 @@ $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).

module gettext;

/// Optional attribute categories.
enum Tr {
note, /// Pass a note to the translator.
context /// Disambiguate by giving a context.
/** $(NEVER_DOCUMENT) */ // Obsolete API
enum Tr {note, context}
private Context theContext(string[Tr] attributes)
{
if (auto c = Tr.context in attributes)
return Context(*c);
return Context(null);
}
private Comment theComment(string[Tr] attributes)
{
if (auto n = Tr.note in attributes)
return Comment(*n);
return Comment(null);
}

/// A comment passed to the translator.
struct Comment
{
private string comment = null;
}
/// A Context is used to disambiguate identical strings with different meanings.
struct Context
{
private string context = null;
}

version (xgettext) // String extraction mode.
Expand Down Expand Up @@ -300,17 +320,49 @@ version (xgettext) // String extraction mode.
return message;
}

/** $(NEVER_DOCUMENT) */
/** $(NEVER_DOCUMENT) */ // Obsolete API
template tr(string singular, string[Tr] attributes = null,
int line = __LINE__, string file = __FILE_FULL_PATH__, string mod = __MODULE__, string func = __FUNCTION__)
{
alias tr = tr!(singular, null, attributes,
line, file, mod, func);
}

/** $(NEVER_DOCUMENT) */
/** $(NEVER_DOCUMENT) */ // Obsolete API
template tr(string singular, string plural, string[Tr] attributes = null,
int line = __LINE__, string file = __FILE_FULL_PATH__, string mod = __MODULE__, string func = __FUNCTION__)
{
alias tr = tr!(singular, plural, theContext(attributes), theComment(attributes),
line, file, mod, func);
}

/** $(NEVER_DOCUMENT) */
template tr(string singular, Comment comment = Comment(null),
int line = __LINE__, string file = __FILE_FULL_PATH__, string mod = __MODULE__, string func = __FUNCTION__)
{
alias tr = tr!(singular, null, Context(null), comment,
line, file, mod, func);
}

/** $(NEVER_DOCUMENT) */
template tr(string singular, Context context, Comment comment = Comment(null),
int line = __LINE__, string file = __FILE_FULL_PATH__, string mod = __MODULE__, string func = __FUNCTION__)
{
alias tr = tr!(singular, null, context, comment,
line, file, mod, func);
}

/** $(NEVER_DOCUMENT) */
template tr(string singular, string plural, Comment comment = Comment(null),
int line = __LINE__, string file = __FILE_FULL_PATH__, string mod = __MODULE__, string func = __FUNCTION__)
{
alias tr = tr!(singular, plural, Context(null), comment,
line, file, mod, func);
}

/** $(NEVER_DOCUMENT) */
template tr(string singular, string plural, Context context, Comment comment = Comment(null),
int line = __LINE__, string file = __FILE_FULL_PATH__, string mod = __MODULE__, string func = __FUNCTION__)
{
static struct StrInjector
{
Expand All @@ -335,15 +387,15 @@ version (xgettext) // String extraction mode.
else
return Format.plain;
}
string context()
translatableStrings.require(Key(singular, plural, format, context.context)) ~= reference;
if (comment.comment !is null)
{
if (auto c = Tr.context in attributes)
return *c;
return null;
import std.algorithm : canFind;

const key = Key(singular, plural, format, context.context);
if (!comments.require(key).canFind(comment.comment))
comments[key] ~= comment.comment;
}
translatableStrings.require(Key(singular, plural, format, context)) ~= reference;
if (auto c = Tr.note in attributes)
comments.require(Key(singular, plural, format, context)) ~= *c;
}
}
static if (plural == null)
Expand Down Expand Up @@ -374,28 +426,35 @@ else // Translation mode.
/**
Translate `message`.
This does *not* instantiate a new function for every marked string
(the signature is fabricated for the sake of documentation).
Returns: The translation of `message` if one exists in the selected
language, or `message` otherwise.
See_Also: [selectLanguage]
Examples:
---
writeln(tr!"Translatable message");
writeln(tr!"Translatable message", Comment("A note to the translator"));
writeln(tr!"Translatable message", Context("A different context"));
writeln(tr!"Translatable message", Context("A different context"), Comment("Translate this differently"));
---
*/
template tr(string singular, string[Tr] attributes = null)
template tr(string message, Comment comment = Comment(null))
{
enum tr = TranslatableString(message);
}
/// idem
template tr(string message, Context context, Comment comment = Comment(null))
{
enum tr = TranslatableString(singular, attributes);
enum tr = TranslatableString(message, context);
}
/** $(NEVER_DOCUMENT) */ // Obsolete API
template tr(string message, string[Tr] attributes = null)
{
enum tr = TranslatableString(message, theContext(attributes));
}
/**
Translate a message in the correct plural form.
This does *not* instantiate a new function for every marked string
(the signature is fabricated for the sake of documentation).
The first argument should be in singular form, the second in plural
form. Note that the format specifier `%d` is optional.
Expand All @@ -406,11 +465,24 @@ else // Translation mode.
Examples:
---
writeln(tr!("There is a goose!", "There are %d geese!")(n));
writeln(tr!("There is a goose!", "There are %d geese!", Comment("A note to the translator"))(n));
writeln(tr!("There is a goose!", "There are %d geese!", Context("A different context"))(n));
writeln(tr!("There is a goose!", "There are %d geese!", Context("A different context"), Comment("Translate this differently"))(n));
---
*/
template tr(string singular, string plural, Comment comment = Comment(null))
{
enum tr = TranslatableStringPlural(singular, plural);
}
/// idem
template tr(string singular, string plural, Context context, Comment comment = Comment(null))
{
enum tr = TranslatableStringPlural(singular, plural, context);
}
/** $(NEVER_DOCUMENT) */ // Obsolete API
template tr(string singular, string plural, string[Tr] attributes = null)
{
enum tr = TranslatableStringPlural(singular, plural, attributes);
enum tr = TranslatableStringPlural(singular, plural, theContext(attributes));
}
}

Expand Down Expand Up @@ -450,12 +522,14 @@ a separate function for every string. https://forum.dlang.org/post/t8pqvg$20r0$1
@safe struct TranslatableString
{
private immutable(string)[] seq;
this (string str, string[Tr] attributes = null) nothrow
this (string str)
{
if (auto context = Tr.context in attributes)
str = SOH ~ *context ~ EOT ~ str;
seq = [str];
}
this (string str, Context context)
{
seq = context.context is null ? [str] : [SOH ~ context.context ~ EOT ~ str];
}
this (string[] seq) nothrow
{
this.seq = seq.idup;
Expand Down Expand Up @@ -544,13 +618,16 @@ a separate function for every string. https://forum.dlang.org/post/t8pqvg$20r0$1
@safe struct TranslatableStringPlural
{
string str, strpl;
this(string str, string strpl, string[Tr] attributes = null)
this (string str, string strpl)
{
if (auto context = Tr.context in attributes)
str = SOH ~ *context ~ EOT ~ str;
this.str = str;
this.strpl = strpl;
}
this (string str, string strpl, Context context)
{
this.str = context.context is null ? str : SOH ~ context.context ~ EOT ~ str;
this.strpl = strpl;
}
string opCall(size_t number) const
{
import std.algorithm : findSplitAfter, max, startsWith;
Expand Down
46 changes: 23 additions & 23 deletions tests/showcase/po/nb_NO.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-08-26T14:35:03.4467743Z\n"
"POT-Creation-Date: 2023-08-27T10:52:19.7000605Z\n"
"PO-Revision-Date: 2022-07-01 15:18+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
Expand All @@ -18,54 +18,59 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.1\n"

#: source/app.d-mixin-130:130(main)
#: source/app.d-mixin-136:136(main)
msgid "This is mixed in code."
msgstr "Dette er innblandet kode."

#: source/app.d:10
msgid "Saturday"
msgstr "lørdag"

#: source/app.d:100(main.report)
#: source/app.d:100(main)
#, c-format
msgid "Format the %s"
msgstr "Formatter %sen"

#: source/app.d:106(main.report)
#, c-format
msgid "Last %s, in %s, I ate a muffin."
msgid_plural "Last %s, in %s, I ate %d muffins."
msgstr[0] "Siste %s, i %s, spiste jeg en muffins."
msgstr[1] "Siste %s, i %s, spiste jeg %d muffins."

#: source/app.d:104(main.report)
#: source/app.d:11
msgid "Sunday"
msgstr "søndag"

#: source/app.d:110(main.report)
#, c-format
msgid "I ate a muffin in %1$s on %2$s."
msgid_plural "I ate %3$d muffins in %1$s on %2$s."
msgstr[0] "Jeg spiste en muffins i %1$s på %2$s."
msgstr[1] "Jeg spiste %3$d muffins i %1$s på %2$s."

#: source/app.d:11
msgid "Sunday"
msgstr "søndag"

#: source/app.d:112(main)
#: source/app.d:118(main)
msgid "Copenhagen"
msgstr "København"

#: source/app.d:114(main)
#: source/app.d:120(main)
msgid "Sidney"
msgstr "Sidney"

#: source/app.d:118(main) source/app.d:122(main)
#: source/app.d:124(main) source/app.d:128(main)
msgid "message"
msgstr "beskjed"

#. Notice to translator.
#: source/app.d:126(main)
#: source/app.d:132(main)
#, c-format
msgctxt "software"
msgid "One license."
msgid_plural "%d licenses."
msgstr[0] "En lisens."
msgstr[1] "%d lisenser."

#: source/app.d:127(main)
#: source/app.d:133(main)
#, c-format
msgctxt "driver's"
msgid "One license."
Expand Down Expand Up @@ -162,36 +167,31 @@ msgid "Thursday"
msgstr "torsdag"

#. Proper name. Phonetically: ˈwɔltər braɪt
#: source/app.d:80(main)
#: source/app.d:80(main) source/app.d:81(main)
msgid "Walter Bright"
msgstr "Walter Bright"

#: source/app.d:83(main)
#: source/app.d:84(main)
msgctxt "Menu|File|Open"
msgid "Open"
msgstr "Åpne"

#: source/app.d:84(main)
#: source/app.d:85(main)
msgctxt "Menu|File|Printer|Open"
msgid "Open"
msgstr "Åpne"

#: source/app.d:86(main)
#: source/app.d:87(main) source/app.d:92(main)
msgctxt "document"
msgid "Review the draft."
msgstr "Gjennomgå utkastet."

#. Nautical term! "Draft" = how deep the bottom of the ship is below the water level.
#: source/app.d:87(main)
#: source/app.d:88(main) source/app.d:93(main)
msgctxt "nautical"
msgid "Review the draft."
msgstr "Kontroller dypgangen."

#: source/app.d:9
msgid "Friday"
msgstr "fredag"

#: source/app.d:94(main)
#, c-format
msgid "Format the %s"
msgstr "Formatter %sen"
Loading

0 comments on commit dd94820

Please sign in to comment.