-
Notifications
You must be signed in to change notification settings - Fork 218
Initial Work on Using Translations Via LcfTrans #2287
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
Conversation
mateofio
left a comment
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.
Doing all this text conversion on the fly is going to slow down games that don't use this feature. It also adds a lot of complexity over the code and you're bound to miss a few cases.
Can't we do it on startup? LIke on game initialization, retranslate the whole database. On load map, retranslate all strings in the map data. On load save game, retranslate all strings in the save game etc..
Maybe we can add a nice autogenerated hook to liblcf that lets you iterate over all string members of all lcf struct structs.
|
Hmm, I was thinking the performance hit wouldn't be that big, but then again for things like Nintendo 3DS and Android it might start to matter. Translating the DB on the fly whenever the translation is selected makes sense, although we'd need to reload the original LCF if the translation was changed at runtime (so: slower to switch translations, but that's actually fine). Do you know if the LCF data structure can be modified at runtime? I.e., variables are not "const", and nothing is cached that could interfere with changing the data at runtime? If a hook is added to iterate over all strings, it would make sense to have it include the "contexts" as well --and LcfTrans should probably use it so that both code paths are aligned. Ideally there would be a similar function to iterate + change all strings. |
It's not just performance, there are going to be endless whack-a-mole style bugs where we forget to translate a string or 2 here and there. The key here is that for something like this we have make it systematic so we solve the problem once and only once. Sprinkling
QuickSave method: If you want to support hot reload of translations, then as discussed in irc, one way to go is if you change the translation we can do an automatic quicksave/quickload and reload all the game structures. Then the translation logic is localized to one place and always works. I think it's perfectly acceptable to have to wait for an automatic reload if you change the translation.
Right now they can, but later they will be behind a const facade #2285 . Still this could be modified to allow translation or if we do the quicksave/quickload approach it doesn't matter. Also, some of the save structures are copied into the various classes so getting at all of them isn't really viable.
The hook can be a very general Like this one: Liblcf just provides a mechanism to call a lambda function on all string members, then it's up to player to supply the translate function into the lambda. |
|
Prereplacing will work for most of the cases. The only one that will probably fail is the ShowMessage command because it is multi-line and has other funny corner cases that are out-of-scope right now like "the translation has 5 lines but the original is 4". But except for this one edge-case it will work. This hook callback function will then receive a context. For flexibility it could be the name of the field, so for a lcf::Skill.name the context is "name". I will think about a solution as I don't expect sorlok to dig into our lcf code generator ;) |
|
@fmatthew5876 https://github.com/EasyRPG/liblcf/compare/master...Ghabry:foreachstring?expand=1 usage (not PR'ing yet, do this after StringView and DBString) |
yeah exactly, something like that. (use const F&, not F&&) |
24fa870 to
0712989
Compare
f723e59 to
c625586
Compare
|
I removed all the old runtime changes (except message boxes), so most of the translation now happens in ForEachString(). Still a lot of things to fix, but I made a brief demo of Yume 2kki: Unzip it so that "translations" appears in the game's Project directory, then you can select "en" from the menu. I copied the existing translation strings/pictures from wolfen's translation --this is a tech demo, so please don't redistribute beyond this thread. The cool thing about doing translations this way is that wolfen's translation was made for 0.112b, but you can run my link on 0.113e (or later) since it's just doing string substitution. I can definitely see the value of a system like this for projects that receive updates. By the way, I think I will switch my demo game to something else. Yume 2kki is really interesting, but it doesn't have a lot of "standard" RPG Maker stuff (like combat effects, states, or some menu items), so it doesn't really stress the software. Suggestions are welcome (or maybe I'll just do Don Miguel's demo). |
|
Yeah I made the "-u" update switch mostly for Yume2kki. The updates are so fast that normal translators are always behind because it is so cumbersome to update this. The 0.112 to 0.113 is actually quite interesting for testing the merge algorithm: Btw a tinygettext replacement: Here is the Po parser from lcftrans (used by the update feature so you can consider it tested and working now ;)). (I renamed the class to Dictionary because you already use Translation). Only needs: The starts_with converted to StringView startswith and maybe helpers to find entries. Otherwise is just "call fromPo" and then magic happens. class Entry {
public:
std::string original; // msgid
std::string translation; // msgstr
std::string context; // msgctxt
};
class Dictionary
{
public:
const std::vector<Entry>& GetEntries() const;
static Dictionary FromPo(std::istream& in);
private:
std::vector<Entry> entries;
};
const std::vector<Entry>& Dictionary::GetEntries() const {
return entries;
}
Dictionary Dictionary::FromPo(std::istream& in) {
// Super simple parser.
// Only parses msgstr, msgid and msgctx
Dictionary t;
std::string line;
bool found_header = false;
bool parse_item = false;
Entry e;
auto extract_string = [&line](int offset) {
std::stringstream out;
bool slash = false;
bool first_quote = false;
for (char c : line.substr(offset)) {
if (c == ' ' && !first_quote) {
continue;
} else if (c == '"' && !first_quote) {
first_quote = true;
continue;
}
if (!slash && c == '\\') {
slash = true;
} else if (slash) {
slash = false;
switch (c) {
case '\\':
out << c;
break;
case 'n':
out << '\n';
break;
case '"':
out << '"';
break;
default:
std::cerr << "Parse error " << line << " (" << c << ")\n";
break;
}
} else {
// no-slash
if (c == '"') {
// done
return out.str();
}
out << c;
}
}
std::cerr << "Parse error: Unterminated line" << line << "\n";
return out.str();
};
auto read_msgstr = [&]() {
// Parse multiply lines until empty line or comment
e.translation = extract_string(6);
while (std::getline(in, line, '\n')) {
if (line.empty() || starts_with(line, "#")) {
break;
}
e.translation += extract_string(0);
}
parse_item = false;
t.addEntry(e);
};
auto read_msgid = [&]() {
// Parse multiply lines until empty line or msgstr is encountered
e.original = extract_string(5);
while (std::getline(in, line, '\n')) {
if (line.empty() || starts_with(line, "msgstr")) {
read_msgstr();
return;
}
e.original += extract_string(0);
}
};
while (std::getline(in, line, '\n')) {
if (!found_header) {
if (starts_with(line, "msgstr")) {
found_header = true;
}
continue;
}
if (!parse_item) {
if (starts_with(line, "msgctxt")) {
e.context = extract_string(7);
parse_item = true;
} else if (starts_with(line, "msgid")) {
parse_item = true;
read_msgid();
}
} else {
if (starts_with(line, "msgid")) {
read_msgid();
} else if (starts_with(line, "msgstr")) {
read_msgstr();
}
}
}
return t;
} |
|
Awesome, thanks! I'll integrate the PO reader. |
351c00c to
9214b0d
Compare
|
You can't use dynamic cast. It requires rtti which results in binary bloat (and I forgot to disable rtti+exceptions in the web player. Huge waste of space). |
|
From an esthethic POV I like that the Language selection is part of the title scene and just spawns a new command box :). Works nice in Yume2kki! |
|
I recorded a video here: Nice job! yume2kki-translation-0.6.3.zip @sorlok I general imo the folder should be "Languages". In other user interfaces you usually see the word "Languages", not "Translations". (same for the folder -> "languages") E.g. for each language a "Meta.ini" file: |
Removed the dynamic cast. Right now I just scan both maps and common events (since there's no good way to tell). That will work fine for now. |
|
Languages should load correctly now regardless of case. Also, the directory was renamed to "Languages", and the Meta.ini file works as described. There is also Help text shown. |
|
You can't use exceptions. They are disabled on the same performs as rtti ;). |
Just as a note, some games ( #2035 (comment) ) will require additional tweaks to integrate additional options. A possible approach to work around this without modifying the original game data: From the built-in system graphics in Player, get the blinking arrow sprites used for scrollable window selectables and add them when there are more than three options (e.g. Translations, Options), then when the selectable goes further the quit, display the built-in system window over the original with the 3 original selectors (with some built-in names) + the additional ones. When to override with built-in system graphics? It's likely 99% safe for games without empty strings in new game and quit game. It's unlikely games exist with transparent color for picture, selectable inner content and window background (it could be checked if some game actually uses this trick, though, if worth). Some games skip the title scene (AEP patches and modern 2k[3]E) which will require some alternate approach on how to access translations without that scene. What about injecting it when there's is an open video options call from those games? |
|
Example export of RPG Advocate's Aurora's Tear translation is now "done": ...however, note that this is still not a 1.0 translation, since the way Choice text is stored will change in LcfTrans (to be independent of the Message text). Once that happens, I'll update this. |
|
Aurora's Tear translation is now at 1.0: Keep in mind that RPG Advocate's translation was not 100% to begin with --this simply copies over everything they worked on. I need to rebase this branch and do a minor code cleanup, then it will be ready to merge. |
|
Hi all, I am currently on a bit of a vacation, so updates on this will be slow. I will try to do a final pass sometime in the next few weeks. |
f029379 to
3037b1a
Compare
|
Ok, I managed to get some time to look at this. I've rebased it and recompiled, and it all works as expected. Note that @fmatthew5876's outstanding request was dealt with some time ago (we modify the LDB/Maps at load instead of when strings are requested). So this Pull Request should be good to go! |
|
@fmatthew5876 , @Ghabry (just bumping this up in case you missed it). |
|
sorry, I'm kinda overloaded with work since a month, so I can't really focus on Player right now... So had no time to check this yet again :/ |
|
No worries, I'll check again in a few weeks. Best of luck! |
Ghabry
left a comment
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.
Well for me this all works excellent. I want to look at RewriteEventCommandMessage again later. This function is really complecated, maybe can be simplified ^^
Please make the requested changes in a new commit
src/scene_title.h
Outdated
|
|
||
| /** | ||
| * Moves a window (typically the New/Continue/Quit menu) to the middle or bottom-center of the screen. | ||
| * @param centerVertical If true, the menu will be centered vertically. Otherwise, it will be at the bottom of the screen. |
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.
lacks @window documentation.
Also in general your naming convention again: centerVertical -> center_vertical
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.
Fixed missing documentation.
Fixed all the camel case names I could find.
src/scene_title.h
Outdated
| * Picks a new language based and switches to it. | ||
| * @param langStr If the empty string, switches the game to 'No Translation'. Otherwise, switch to that translation by name. | ||
| */ | ||
| void ChangeLanguage(const std::string& langStr); |
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.
lang_str
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.
Fixed
src/scene_title.h
Outdated
| int continue_game = 1; | ||
| int import = -1; | ||
| int translate = -1; | ||
| int exit = 2; |
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.
don't align the "=", just move them to the left next to the var name.
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.
Fixed
src/translation.cpp
Outdated
|
|
||
| // Message box commands to remove a message box or add one in place. | ||
| #define TRCUST_REMOVEMSG "__EASY_RPG_CMD:REMOVE_MSGBOX__" | ||
| #define TRCUST_ADDMSG "__EASY_RPG_CMD:ADD_MSGBOX__" |
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.
Applies to remove and add: Is this still used? I see them checked in ifs but never assigned?
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.
These strings are supposed to be added by the translator. Here's how it works; let's say I have a message box:
msgid ""
"Hero: Hello there!"
...and I want that message box to be removed from the translation entirely. In that case, I replace it with:
msgstr ""
"__EASY_RPG_CMD:REMOVE_MSGBOX__"
Let's say, however, that in the target language it makes more sense for the other party to shout at the hero first. So I need a new message box to appear BEFORE the current one. I'll replace it with:
msgstr ""
"Townsperson: Mr. Hero, hello!"
"__EASY_RPG_CMD:ADD_MSGBOX__"
"Hero: Hello there!"
This will ADD a message box to the event list, so now we'll have two message boxes (rather than a single two-line message box).
They are magic strings, unfortunately, but I think they're pretty unlikely to appear in source games. (And I can't think of a more robust way to do this that interacts cleanly with gettext).
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.
ah now I get it so that's some kind of in-band signaling. Will see if I can find a way to improve this.
For "insert new page" we already have the the form feed "\f" (needed for some battle messages) but I guess this will need changes to the po parser.
For "delete message" one could use gettext flags. They are marked via #,, something like #, easyrpg-delete (see https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html). Will also need parser changes (to ensure lcftrans keeps them)
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.
Yep, that's right.
I think I might prefer to keep the current strings, since they are very self-descriptive for translators, and I don't think that "\f" or gettext flags will clean up our own processing code. But this is certainly up for debate.
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.
at least \f will not easily work because our code filters away control characters... Have to sleep about this ^^'
src/translation.cpp
Outdated
| #define TRCUST_ADDMSG "__EASY_RPG_CMD:ADD_MSGBOX__" | ||
|
|
||
|
|
||
| std::string Tr::TranslationDir() { |
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 a getter, call it GetTranslationDir
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.
Fixed
| CommandIterator(std::vector<lcf::rpg::EventCommand>& commands) : commands(commands) {} | ||
|
|
||
| /// Returns true if the index is past the end of the command list | ||
| bool Done() const { |
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.
Wrong doc comments, should be /** */
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.
Fixed all usages of ///
src/translation.h
Outdated
| struct Language { | ||
| std::string langDir; // Language directory (e.g., "en", "English_Localization") | ||
| std::string langName; // Display name for this language (e.g., "English") | ||
| std::string langDesc; // Helper text to show when the menu is highlighted |
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.
should be lang_dir, lang_name, lang_desc
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.
Fixed
src/translation.h
Outdated
| * @param langId The ID of the language to parse, or "" for Default (no parsing is done) | ||
| * @return True if the language directory was found; false otherwise | ||
| */ | ||
| bool ParseLanguageFiles(const std::string& langId); |
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.
lang_id
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.
Fixed
src/translation.h
Outdated
| std::string translationRootDir; | ||
|
|
||
| // The translation we are currently showing (e.g., "English_1") | ||
| std::string currLanguage; |
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_root_dir, current_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.
Fixed
| // A note on this function: it is (I feel) necessarily complicated, given what it's actually doing. | ||
| // I've tried to abstract most of this complexity away behind an "iterator" interface, so that we do not | ||
| // have to track the current index directly in this function. | ||
| CommandIterator commands(commandsOrig); |
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.
Marking this as "Reviewing later", can you explain me first how this TRCUST_REMOVEMSG stuff works? Considering that this is never pushed anywhere I'm confused.
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.
See the earlier comment about the translator adding the strings into the translated text.
Given that, the basic idea is to take a CommandIterator on the command stream, and then .insert() and .remove() as needed to get the requested translation. It's fairly generic around MessageBoxes, and a little bit stricter around Choices. I think the comments do a reasonable job of explaining what's going on, but let me know if there are some areas that are not clear and I'll tidy up the docs.
I did a pass to simplify the function (a month ago); it's possible it can be simplified a little further, or we could encapsulate some of these activities (e.g., "trim down to 4 lines") into their own tiny functions, if that helps.
1807c99 to
e2fd5c6
Compare
|
Thanks for the feedback; all items were fixed (and I added two major comments describing how add/remove work). I also rebased, and loading maps now crashes on Linux (see below), but since it does this for new games with translations turned off, I think it is probably not related to my minor code cleanup: |
|
try updating to the latest liblcf version. Here it works. |
Yep, that fixed it, thanks. (I had to do a clean rebuild of both sources.) |
|
I've been thinking about
The final decision is of course up to EasyRPG devs. |
|
Maybe make the commands less clunky by using something like |
|
That sounds good; I'll change the commands to XML-like later today. |
commit fd9b9e4 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Fri Oct 16 13:48:31 2020 -0400 Quick fix for new LcfTrans organization of files commit b61a5e5 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Tue Sep 29 01:24:07 2020 -0400 Fix a minor issue with standalone choice boxes commit 359315a Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Tue Sep 29 01:09:34 2020 -0400 Minor fix for case where a message box at the end of the event list needs to be expanded commit 249f370 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 21:21:56 2020 -0400 Add in Treemap translations commit de06e5b Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 21:10:19 2020 -0400 Clean up the translation code a bit commit 3fcd92a Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 19:23:31 2020 -0400 Update doc strings commit 5a0607d Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 17:24:56 2020 -0400 Documentation and cleanup commit 1931fe9 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 17:11:26 2020 -0400 More consistent naming of variables, where possible commit 923e135 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 16:59:33 2020 -0400 Revert game_interpreter and game_system changes commit 67aa5d0 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 16:57:37 2020 -0400 Cleanup commit 0e5e5ad Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 16:53:33 2020 -0400 Revert async_handler changes commit d5cd3c8 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 10:08:09 2020 -0400 Switch to Before logic and fix memory error commit cf661d4 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 01:38:10 2020 -0400 Initial work on AsyncRequest for audio commit 1cd38dd Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 00:51:26 2020 -0400 Some indenting cleanup commit 91741b3 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 28 00:45:43 2020 -0400 Allow inserting and removing message boxes with the same style. commit e2e5d6c Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Sun Sep 27 23:27:48 2020 -0400 Rewrite battle events, and fix a bug where no translation overwrote the original translation with nothing commit 9c1df5d Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Sun Sep 27 22:44:02 2020 -0400 Fix changes from merging commit f6b1617 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Sun Sep 27 22:21:54 2020 -0400 Fix common/battle import, and actually use common commit 2508f77 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Sat Sep 26 22:54:13 2020 -0400 Fix invalid choice parameter option, and remove dead code commit 390dcea Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Sat Sep 26 22:32:28 2020 -0400 Avoid RTP search on translated pictures commit 7caa40c Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Sat Sep 26 20:51:27 2020 -0400 Better translation iterator-like class; fixed an issue with indexes not being updated commit 3c057ed Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Fri Sep 25 20:16:47 2020 -0400 Translate the map directly when switching to the map, instead of trying to do it at runtime when the message box pops up commit b35f1df Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Thu Sep 24 03:01:27 2020 -0400 Choice boxes work, although it's a little hacky. commit 4052ad6 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Thu Sep 24 02:30:54 2020 -0400 Fix a few errors with messages not loading if substitutions are present (and Game_Actors not resetting substitutions). commit d4b7e90 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Tue Sep 15 00:18:21 2020 -0400 Load the Meta.ini file to retrieve the translation's name, and show Translation help text when appropriate. commit 118d3ed Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 14 23:05:34 2020 -0400 Fix capitalization issues for language and root directory. commit 37d1622 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 14 22:18:57 2020 -0400 Remove dynamic cast and replace GetEntry() with a faster TranslateString() commit 2acfba0 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Mon Sep 14 01:20:10 2020 -0400 Put CommandIndices into a struct to ensure nothing is missed on reset commit e7e9f12 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Sun Sep 13 22:48:00 2020 -0400 Added in Ghabry's dictionary parsing code and removed tinygettext commit 97a819d Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Sun Sep 13 18:13:00 2020 -0400 Always reload the database on translation change to deal with cached entries commit 2f6d844 Author: Seth N. Hetu <seth.hetu@gmail.com> Date: Sun Sep 13 01:43:27 2020 -0400 Second draft Translation (Localization) code. Rewrites the database for non-messages. Hotswaps messages and assets.
…d on documentation
57ef3df to
87c8d99
Compare
|
Changes made; we now use The sample translation of Aurora's Tear has also been updated to match: As far as I'm concerned, this feature is ready. |
Changes have been addressed with ForEachString() as suggested
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.
Except for Scene_Title this is minimally invasive as all rewrites are on Database/Map load.
<easyrpg:delete_page> and <easyrpg:new_page> also work as intended and help alot when the target language has a much shorter (delete unneeded messages) or a much longer (add more pages to handle e.g. a "Actor name" on Line 1) translation.
Imo this is a really good job and the only way to find more issues is testing this in the wild. 👍
DO NOT MERGE: This is for discussion only.This is a draft PR;see Issue #797 for the initial discussion. Further code discussion will go here.