New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Client-side translations: gettext and plural support #13687
base: master
Are you sure you want to change the base?
Conversation
db06c26
to
0819a09
Compare
I will have time to review this soon, so please rebase. I've given it a quick look through, and have two comments:
|
0819a09
to
574b948
Compare
I just rebased it, it should be good now. I'll try to write some unit tests and move the parser to its own class, it might be a bit harder to do for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicking.
line.resize(line.length() - 1); | ||
|
||
if (line.empty() || line[0] == '#') | ||
continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since xgettext
can generate fuzzy entries it would make sense to check for (and skip) those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the documentation of gettext, it seems to say that fuzzy entries are considered as normal entries for most purposes, so I wouldn't drop them. In particular, the .po
to .mo
compilation would use them, so we should use them as well if we don't want to introduce a discrepancy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the documentation of gettext, it seems to say that fuzzy entries are considered as normal entries for most purposes, so I wouldn't drop them.
While that is the case, the problem with fuzzy matching is that it is enabled by default in msgmerge
(unless the -N
flag is passed) and can often match entries with different (sometimes opposite, e.g. "enabled" and "disabled") meanings while assigning the same translation if one is missing, and there are also reasons to generate fuzzy entries, e.g. if the source string does not change much (adding a period to the end of the source string) or to give translators a base string to modify/work with.
In particular, the
.po
to.mo
compilation would use them, so we should use them as well if we don't want to introduce a discrepancy.
msgfmt
does not seem to include fuzzy entries by default (there is the -f
flag for including them which GNU does not recommend using).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the documentation! I'll implement matching the fuzzy entries in that case.
I was referring to https://github.com/minetest/minetest/blob/master/util/mod_translation_updater.py Makes sense for someone to update that in another PR though |
description = "Test translations", | ||
privs = {}, | ||
func = function(name, param) | ||
minetest.chat_send_player(name, "Please ensure your locale is set to \"fr\"") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minetest.chat_send_player(name, "Please ensure your locale is set to \"fr\"") | |
if minetest.get_player_information(name).lang_code ~= "fr" then | |
return false, "Please restart Minetest with language = fr" | |
end | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why you resolved the comment without applying the suggestion? Please reply if you don't like a suggestion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I applied the suggestion, but I must have forgotten to pull before rebasing, deleting the commit. I'll commit it again, thanks for catching it!
Although, now that I'm thinking about it, we really want to test this both with fr
and default locale (to see both untranslated and translated strings), I'll need to check what lang_code
is in that case.
Hmmmm, I'm not why the CI fails, do you have an idea? |
191b397
to
7d842b2
Compare
msgid "Testing .mo files: untranslated" | ||
msgstr "Testing .mo files: translated" | ||
|
||
msgctxt "testtranslations" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the duplicate msgctxt
?
Looks like this is required on every translation to set the text domain, are you sure gettext requires this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this msgctxt is required for every message, this should be checked by the engine and warned/errored about
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this file is used to generate locale/translation_mo.fr.mo
? The translation file reader uses the base filename (here: translation_mo
) as the text domain if one is not specified.
The GNU Gettext doc on PO files does not seem clear about about the scope (i.e. the entries that the msgctxt
applies to) of msgctxt
. However, considering that
- The documentation states that "an empty context string and an absent msgctxt line do not mean the same thing";
- Entries with
msgctxt
are considered as "entries with a context specifier"; and - The entire documentation does not mention "scopes" of other specifiers (that said, having
msgid
andmsgtxt
to multiple entries would make little sense anyway)
It would be safe to assume that entries without an explicit context specifier would be considered as entries without context information, which, combined with the fact that Minetest does not differentiate between "text domain" and "context" (and, in this PR, uses the former if the latter is absent), means that any entry without an explicit msgctxt
specifier would be put under the default (here: translation_mo
) domain, which would explain the duplicate msgctxt
as later entries would otherwise be put into the translation_mo
domain.
It is probably also worth noting that the generated .mo
file includes context information on every entry as well.
That said, while entries without msgctxt
are allowed (see #13687 (comment)), there does not seem to be any testcase/testfile related to this, so it would make sense to expand the test file to include entries without an explicit context specifier.
@@ -2551,8 +2551,8 @@ bool Server::addMediaFile(const std::string &filename, | |||
".png", ".jpg", ".bmp", ".tga", | |||
".ogg", | |||
".x", ".b3d", ".obj", | |||
// Custom translation file format | |||
".tr", | |||
// Translation file format |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Translation file format | |
// Translation file formats |
@@ -0,0 +1,2 @@ | |||
# textdomain: testtranslations | |||
Testing .tr files: untranslated=Testing .tr files: translated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing EOF new line
Testing .tr files: untranslated=Testing .tr files: translated | |
Testing .tr files: untranslated=Testing .tr files: translated | |
```lua | ||
local S, NS = minetest.get_translator() | ||
NS("@1 file", "@1 files", n, tostring(n)) | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code blocks need blank lines around them
```lua | |
local S, NS = minetest.get_translator() | |
NS("@1 file", "@1 files", n, tostring(n)) | |
``` | |
```lua | |
local S, NS = minetest.get_translator() | |
NS("@1 file", "@1 files", n, tostring(n)) |
Old translation file format | ||
--------------------------- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Old translation file format | |
--------------------------- | |
Old translation file format (.tr) | |
--------------------------------- |
Translation file format | ||
----------------------- | ||
Old translation file format | ||
--------------------------- | ||
|
||
A translation file has the suffix `.[lang].tr`, where `[lang]` is the language |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A translation file has the suffix `.[lang].tr`, where `[lang]` is the language | |
Note: You should use the Gettext translation file format instead. | |
A translation file has the suffix `.[lang].tr`, where `[lang]` is the language |
description = "Test translations", | ||
privs = {}, | ||
func = function(name, param) | ||
minetest.chat_send_player(name, "Please ensure your locale is set to \"fr\"") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why you resolved the comment without applying the suggestion? Please reply if you don't like a suggestion
Here's a patch to fix content translation with .po files commit cfb914572ebd41066587f59af1022ecee87529f3
Author: rubenwardy <rw@rubenwardy.com>
Date: Sun Mar 24 18:24:30 2024 +0000
Fix content translation and Gettext format
diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp
index 4e22c3ae8..529079ea2 100644
--- a/src/gui/guiEngine.cpp
+++ b/src/gui/guiEngine.cpp
@@ -214,15 +214,28 @@ GUIEngine::GUIEngine(JoystickController *joystick,
/******************************************************************************/
-std::string findLocaleFileInMods(const std::string &path, const std::string &filename)
+std::string findLocaleFileWithExtension(const std::string &path)
+{
+ if (fs::PathExists(path + ".mo"))
+ return path + ".mo";
+ if (fs::PathExists(path + ".po"))
+ return path + ".po";
+ if (fs::PathExists(path + ".tr"))
+ return path + ".tr";
+ return "";
+}
+
+
+/******************************************************************************/
+std::string findLocaleFileInMods(const std::string &path, const std::string &filename_no_ext)
{
std::vector<ModSpec> mods = flattenMods(getModsInPath(path, "root", true));
for (const auto &mod : mods) {
- std::string ret = mod.path + DIR_DELIM "locale" DIR_DELIM + filename;
- if (fs::PathExists(ret)) {
+ std::string ret = findLocaleFileWithExtension(
+ mod.path + DIR_DELIM "locale" DIR_DELIM + filename_no_ext);
+ if (!ret.empty())
return ret;
- }
}
return "";
@@ -235,19 +248,26 @@ Translations *GUIEngine::getContentTranslations(const std::string &path,
if (domain.empty() || lang_code.empty())
return nullptr;
- std::string filename = domain + "." + lang_code + ".tr";
- std::string key = path + DIR_DELIM "locale" DIR_DELIM + filename;
+ std::string filename_no_ext = domain + "." + lang_code;
+ std::string key = path + DIR_DELIM "locale" DIR_DELIM + filename_no_ext;
if (key == m_last_translations_key)
return &m_last_translations;
std::string trans_path = key;
- ContentType type = getContentType(path);
- if (type == ContentType::GAME)
- trans_path = findLocaleFileInMods(path + DIR_DELIM "mods" DIR_DELIM, filename);
- else if (type == ContentType::MODPACK)
- trans_path = findLocaleFileInMods(path, filename);
- // We don't need to search for locale files in a mod, as there's only one `locale` folder.
+
+ switch (getContentType(path)) {
+ case ContentType::GAME:
+ trans_path = findLocaleFileInMods(path + DIR_DELIM "mods" DIR_DELIM,
+ filename_no_ext);
+ break;
+ case ContentType::MODPACK:
+ trans_path = findLocaleFileInMods(path, filename_no_ext);
+ break;
+ default:
+ trans_path = findLocaleFileWithExtension(trans_path);
+ break;
+ }
if (trans_path.empty())
return nullptr; |
Here's a zip file for testing gettext content translation gettext_content_translation.zip Not sure why this happens just for games, will check master as well. See list title vs title on the right |
The reason I say this is because it's a good idea to have unit tests when writing a parser for a file format as it gives some confidence that the parser actually works. The tests should cover a lot of edge cases (missing |
This is a rebase of #12446.
This PR is complete, see instructions on the previous PR for how to test.
How to test
For now, take a mod using client-side translations, and replace the .tr file with a .po one, with the following entries for each original translated string: