From 4fac960cf46a6e83cca3bbdbd650290548f4209d Mon Sep 17 00:00:00 2001 From: Sven Eberhardt Date: Sun, 7 May 2017 14:25:03 -0400 Subject: [PATCH] Editor: Add localized string support --- CMakeLists.txt | 3 + docs/sdk/script/fn/GetTranslatedString.xml | 59 ++++++ docs/sdk/script/fn/Translate.xml | 1 + planet/System.ocg/LanguageDE.txt | 4 + planet/System.ocg/LanguageUS.txt | 4 + src/editor/C4ConsoleQtLocalizeString.cpp | 171 ++++++++++++++++ src/editor/C4ConsoleQtLocalizeString.h | 57 ++++++ src/editor/C4ConsoleQtLocalizeString.ui | 130 +++++++++++++ src/editor/C4ConsoleQtPropListViewer.cpp | 214 ++++++++++++++++++--- src/editor/C4ConsoleQtPropListViewer.h | 26 ++- src/game/C4Game.cpp | 80 ++++++++ src/game/C4Game.h | 4 + src/game/C4GameScript.cpp | 7 + src/script/C4StringTable.cpp | 3 + src/script/C4StringTable.h | 3 + 15 files changed, 734 insertions(+), 32 deletions(-) create mode 100644 docs/sdk/script/fn/GetTranslatedString.xml create mode 100644 src/editor/C4ConsoleQtLocalizeString.cpp create mode 100644 src/editor/C4ConsoleQtLocalizeString.h create mode 100644 src/editor/C4ConsoleQtLocalizeString.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 225df50e4..8eb4340dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -758,6 +758,9 @@ if(WITH_QT_EDITOR) src/editor/C4ConsoleQtNewScenario.cpp src/editor/C4ConsoleQtNewScenario.h src/editor/C4ConsoleQtNewScenario.ui + src/editor/C4ConsoleQtLocalizeString.cpp + src/editor/C4ConsoleQtLocalizeString.h + src/editor/C4ConsoleQtLocalizeString.ui src/editor/C4ConsoleQtMainWindow.ui src/editor/resource.qrc ${qt_editor_resources} diff --git a/docs/sdk/script/fn/GetTranslatedString.xml b/docs/sdk/script/fn/GetTranslatedString.xml new file mode 100644 index 000000000..e43bab624 --- /dev/null +++ b/docs/sdk/script/fn/GetTranslatedString.xml @@ -0,0 +1,59 @@ + + + + + + GetTranslatedString + Script + Strings + 8.0 OC + + string + + + any + string_data + Either a string or a proplist containing multiple translations of a string. If a string or nil is passed, the parameter is returned directly. If a proplist is passed, the value corresponding to the selected language (or a fallback) is returned. + + + + Returns a string corresponding to the user's selected language. For string_data, the expected format is { Function="Translate", DE="Hallo, Welt", US="Hello, World"}. If no matching entry is found or it is nil, then another language string is returned as a fallback. + + + Log(GetTranslatedString({ Function="Translate", DE="Dies ist ein Test.", US="This is a test."})); + Logs either "Dies ist ein Test." or "This is a test." depending on the player's language setting. + + + local inscription = ""; + +// Players can read the sign via the interaction bar. +public func IsInteractable() { return true; } + +// Called on player interaction. +public func Interact(object clonk) +{ + if (!clonk) return false; + Dialogue->MessageBox(GetTranslatedString(inscription), clonk, this, clonk->GetController(), true); + return true; +} + +public func SetInscription(to_text) +{ + inscription = to_text ?? ""; + return true; +} + +public func Definition(def) +{ + // Inscription props + if (!def.EditorProps) def.EditorProps = {}; + def.EditorProps.inscription = { Name="Inscription", Type="string", Set="SetInscription", Save="Inscription", Translatable=true }; +} + Code for a signpost. The string editor property with setting Translatable=true provides a translation proplist in the correct format automatically. + + + Translate + + Sven22017-05 + diff --git a/docs/sdk/script/fn/Translate.xml b/docs/sdk/script/fn/Translate.xml index 82f5b5f16..1e2965709 100644 --- a/docs/sdk/script/fn/Translate.xml +++ b/docs/sdk/script/fn/Translate.xml @@ -34,6 +34,7 @@ MsgOnFire3=Oops, I dropped my lighter! When the clonk catches fire, the engine calls Incineration() in the clonk and in this example, one of the four above messages is displayed at random. + GetTranslatedString Isilkor2009-11 Newton2011-06 diff --git a/planet/System.ocg/LanguageDE.txt b/planet/System.ocg/LanguageDE.txt index 83e209f33..8a7b587fe 100644 --- a/planet/System.ocg/LanguageDE.txt +++ b/planet/System.ocg/LanguageDE.txt @@ -32,6 +32,8 @@ IDS_BTN_YES=Ja IDS_CHAT_NOTCONNECTED=nicht verbunden IDS_CHAT_SERVER=Server IDS_CNS_ACTION=Aktivität: +IDS_CNS_ADDLANGUAGE=Sprache hinzufügen +IDS_CNS_ADDLANGUAGEID=Sprach-ID (z.B. DE, US, FR) IDS_CNS_ALLOBJECTS=Alle Objekte IDS_CNS_ARRAYADD=Element hinzufügen IDS_CNS_ARRAYEDIT=Array @@ -100,6 +102,7 @@ IDS_CNS_SHOWHELP=Hilfetexte anzeigen IDS_CNS_SHOWHELPTIP=Aktiviert oder deaktiviert Objektbeschreibungen und Tooltip-Marker (?) im Objekteigenschaftsdialog. IDS_CNS_TEMPLATE=Vorlage IDS_CNS_TITLE=Titel +IDS_CNS_TRANSLATE=Übersetzung IDS_CNS_TRUE=Ja IDS_CNS_TYPE=Typ: %s (%s) IDS_CNS_VALUE=Wert @@ -398,6 +401,7 @@ IDS_ERR_HELPCMD=Grundlegende Befehle im IRC-Chat:|/join [Chatraum] - Neuen Chatr IDS_ERR_INITFONTS=Fehler bei der Schriftinitialisierung IDS_ERR_INSUFFICIENTPARAMETERS=/%s: fehlende Parameter IDS_ERR_INVALIDCHANNELNAME=Kein gültiger Chat-Kanal. +IDS_ERR_INVALIDLANGUAGEID=Ungültige Sprach-ID. IDS_ERR_INVALIDNICKNAME=Unzulässiger Kurzname. IDS_ERR_INVALIDNICKNAME2=/%s: unzulässiger Kurzname IDS_ERR_INVALIDPASSWORDMAX31CHARA=Nicht zulässiges Passwort: maximal 31 Zeichen, keine Leerzeichen. diff --git a/planet/System.ocg/LanguageUS.txt b/planet/System.ocg/LanguageUS.txt index 334757331..8f6f09c96 100644 --- a/planet/System.ocg/LanguageUS.txt +++ b/planet/System.ocg/LanguageUS.txt @@ -32,6 +32,8 @@ IDS_BTN_YES=Yes IDS_CHAT_NOTCONNECTED=not connected IDS_CHAT_SERVER=Server IDS_CNS_ACTION=Action: +IDS_CNS_ADDLANGUAGE=Add language +IDS_CNS_ADDLANGUAGEID=Language ID (e.g. DE, US, FR) IDS_CNS_ALLOBJECTS=All objects IDS_CNS_ARRAYADD=Add item IDS_CNS_ARRAYEDIT=Array @@ -100,6 +102,7 @@ IDS_CNS_SHOWHELP=Show help texts IDS_CNS_SHOWHELPTIP=Activates or deactivates object descriptions and tooltip markers (?) in the object property dialogue. IDS_CNS_TEMPLATE=Template IDS_CNS_TITLE=Title +IDS_CNS_TRANSLATE=Translation IDS_CNS_TRUE=Yes IDS_CNS_TYPE=Type: %s (%s) IDS_CNS_VALUE=Value @@ -398,6 +401,7 @@ IDS_ERR_HELPCMD=Basic commands in the IRC-chat:|/join [channel] - Enter a new ch IDS_ERR_INITFONTS=Error initializing fonts IDS_ERR_INSUFFICIENTPARAMETERS=/%s: insufficient parameters IDS_ERR_INVALIDCHANNELNAME=Invalid channel name. +IDS_ERR_INVALIDLANGUAGEID=Invalid language ID. IDS_ERR_INVALIDNICKNAME=Invalid nickname. IDS_ERR_INVALIDNICKNAME2=/%s: invalid nick name IDS_ERR_INVALIDPASSWORDMAX31CHARA=Invalid password. Maximum 31 characters. No spaces allowed. diff --git a/src/editor/C4ConsoleQtLocalizeString.cpp b/src/editor/C4ConsoleQtLocalizeString.cpp new file mode 100644 index 000000000..cb00e25fc --- /dev/null +++ b/src/editor/C4ConsoleQtLocalizeString.cpp @@ -0,0 +1,171 @@ +/* +* OpenClonk, http://www.openclonk.org +* +* Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ +* Copyright (c) 2013, The OpenClonk Team and contributors +* +* Distributed under the terms of the ISC license; see accompanying file +* "COPYING" for details. +* +* "Clonk" is a registered trademark of Matthes Bender, used with permission. +* See accompanying file "TRADEMARK" for details. +* +* To redistribute this file separately, substitute the full license texts +* for the above references. +*/ + +/* String localization editors */ + +#include "C4Include.h" +#include "script/C4Value.h" +#include "config/C4Config.h" +#include "editor/C4ConsoleQtLocalizeString.h" +#include "c4group/C4Language.h" + + +/* Single string editor */ + +C4ConsoleQtLocalizeStringDlg::C4ConsoleQtLocalizeStringDlg(class QMainWindow *parent_window, const C4Value &translations) + : QDialog(parent_window, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint) + , translations(translations) +{ + ui.setupUi(this); + // Add language editors + int32_t lang_index = 0; + C4LanguageInfo *lang_info; + while (lang_info = ::Languages.GetInfo(lang_index++)) + { + AddEditor(lang_info->Code, lang_info->Name); + } + // Fill in values + C4PropList *translations_proplist = translations.getPropList(); + assert(translations_proplist); + for (C4String *lang_str : translations_proplist->GetSortedLocalProperties(false)) + { + if (lang_str->GetData().getLength() == 2) + { + C4Value text_val; + if (translations_proplist->GetPropertyByS(lang_str, &text_val)) + { + C4String *text = text_val.getStr(); + if (text) + { + QLineEdit *editor = GetEditorByLanguage(lang_str->GetCStr()); + if (!editor) + { + // Unknown language. Just add an editor without language name. + editor = AddEditor(lang_str->GetCStr(), nullptr); + } + editor->setText(QString(text->GetCStr())); + } + } + } + } + // Size + adjustSize(); + setMinimumSize(size()); + // Focus on first empty editor + if (edited_languages.size()) + { + edited_languages.front().value_editor->setFocus(); // fallback to first editor + for (const auto & langs : edited_languages) + { + if (!langs.value_editor->text().length()) + { + langs.value_editor->setFocus(); + break; + } + } + } +} + +void C4ConsoleQtLocalizeStringDlg::DoError(const char *msg) +{ + QMessageBox::critical(this, ::LoadResStr("IDS_ERR_TITLE"), QString(msg)); +} + +QLineEdit *C4ConsoleQtLocalizeStringDlg::AddEditor(const char *language, const char *language_name) +{ + assert(!GetEditorByLanguage(const char *language)); + // Add editor widgets + int32_t row = edited_languages.size(); + QString language_label_text(language); + if (language_name) language_label_text.append(FormatString(" (%s)", language_name).getData()); + QLabel *language_label = new QLabel(language_label_text, this); + ui.mainGrid->addWidget(language_label, row, 0); + QLineEdit *value_editor = new QLineEdit(this); + ui.mainGrid->addWidget(value_editor, row, 1); + // Add to list + EditedLanguage new_editor; + SCopy(language, new_editor.language, 2); + new_editor.value_editor = value_editor; + edited_languages.push_back(new_editor); + return value_editor; +} + +QLineEdit *C4ConsoleQtLocalizeStringDlg::GetEditorByLanguage(const char *language) +{ + // Search text editor by language ID + for (const auto & langs : edited_languages) + { + if (!strcmp(langs.language, language)) + { + return langs.value_editor; + } + } + // Not found + return nullptr; +} + +void C4ConsoleQtLocalizeStringDlg::done(int r) +{ + if (QDialog::Accepted == r) // ok was pressed + { + C4PropList *translations_proplist = translations.getPropList(); + assert(translations_proplist); + // Set all translations + for (const auto & langs : edited_languages) + { + // Empty strings are set to nil, because that allows the user to set it to fallback + QString text = langs.value_editor->text(); + if (text.length()) + { + C4Value text_val = C4VString(text.toUtf8()); + translations_proplist->SetPropertyByS(::Strings.RegString(langs.language), text_val); + } + else + { + translations_proplist->ResetProperty(::Strings.RegString(langs.language)); + } + } + } + // Close + QDialog::done(r); +} + +void C4ConsoleQtLocalizeStringDlg::AddLanguagePressed() +{ + bool lang_ok = false; + QRegExpValidator validator(QRegExp("^[a-zA-Z][a-zA-Z]$"), this); + QString lang_id; + while (!lang_ok) + { + bool ok; int q = 0; + lang_id = QInputDialog::getText(this, LoadResStr("IDS_CNS_ADDLANGUAGE"), LoadResStr("IDS_CNS_ADDLANGUAGEID"), QLineEdit::Normal, QString(), &ok); + if (!ok) return; + lang_ok = (validator.validate(lang_id, q) == QValidator::Acceptable); + if (!lang_ok) + { + DoError(LoadResStr("IDS_ERR_INVALIDLANGUAGEID")); + } + } + // Either add or just focus existing editor + QLineEdit *editor = GetEditorByLanguage(lang_id.toUtf8()); + if (!editor) + { + editor = AddEditor(lang_id.toUtf8(), nullptr); + adjustSize(); + setMinimumSize(size()); + } + editor->setFocus(); +} diff --git a/src/editor/C4ConsoleQtLocalizeString.h b/src/editor/C4ConsoleQtLocalizeString.h new file mode 100644 index 000000000..6e9293729 --- /dev/null +++ b/src/editor/C4ConsoleQtLocalizeString.h @@ -0,0 +1,57 @@ +/* +* OpenClonk, http://www.openclonk.org +* +* Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ +* Copyright (c) 2013, The OpenClonk Team and contributors +* +* Distributed under the terms of the ISC license; see accompanying file +* "COPYING" for details. +* +* "Clonk" is a registered trademark of Matthes Bender, used with permission. +* See accompanying file "TRADEMARK" for details. +* +* To redistribute this file separately, substitute the full license texts +* for the above references. +*/ + +/* String localization editors */ + +#ifndef INC_C4ConsoleQtLocalizeString +#define INC_C4ConsoleQtLocalizeString +#ifdef WITH_QT_EDITOR + +#include "C4Include.h" // needed for automoc +#include "editor/C4ConsoleGUI.h" // for glew.h +#include "editor/C4ConsoleQt.h" +#include "ui_C4ConsoleQtLocalizeString.h" + +class C4ConsoleQtLocalizeStringDlg : public QDialog +{ + Q_OBJECT + + Ui::LocalizeStringDialog ui; + C4Value translations; + + struct EditedLanguage + { + char language[3]; + QLineEdit *value_editor; + }; + std::list edited_languages; + +public: + C4ConsoleQtLocalizeStringDlg(class QMainWindow *parent_window, const C4Value &translations); + C4PropList *GetTranslations() const { return translations.getPropList(); } + +private: + void DoError(const char *msg); + QLineEdit *AddEditor(const char *language, const char *language_name); + QLineEdit *GetEditorByLanguage(const char *language); + void done(int r) override; + +protected slots: + void AddLanguagePressed(); +}; + +#endif // WITH_QT_EDITOR +#endif // INC_C4ConsoleQtLocalizeString diff --git a/src/editor/C4ConsoleQtLocalizeString.ui b/src/editor/C4ConsoleQtLocalizeString.ui new file mode 100644 index 000000000..c2e10d7d1 --- /dev/null +++ b/src/editor/C4ConsoleQtLocalizeString.ui @@ -0,0 +1,130 @@ + + + LocalizeStringDialog + + + + 0 + 0 + 863 + 89 + + + + + 500 + 0 + + + + IDS_CNS_TRANSLATE + + + true + + + + + + QLayout::SetDefaultConstraint + + + + + QLayout::SetDefaultConstraint + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + IDS_CNS_ADDLANGUAGE + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + + buttonBox + accepted() + LocalizeStringDialog + accept() + + + 400 + 282 + + + 157 + 274 + + + + + buttonBox + rejected() + LocalizeStringDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + addLanguageButton + pressed() + LocalizeStringDialog + AddLanguagePressed() + + + 78 + 270 + + + 242 + 146 + + + + + + AddLanguagePressed() + + diff --git a/src/editor/C4ConsoleQtPropListViewer.cpp b/src/editor/C4ConsoleQtPropListViewer.cpp index c4a54832d..b40a831ef 100644 --- a/src/editor/C4ConsoleQtPropListViewer.cpp +++ b/src/editor/C4ConsoleQtPropListViewer.cpp @@ -19,6 +19,7 @@ #include "editor/C4ConsoleQtPropListViewer.h" #include "editor/C4ConsoleQtDefinitionListViewer.h" #include "editor/C4ConsoleQtState.h" +#include "editor/C4ConsoleQtLocalizeString.h" #include "editor/C4Console.h" #include "object/C4Object.h" #include "object/C4GameObjects.h" @@ -312,44 +313,191 @@ bool C4PropertyDelegateInt::IsPasteValid(const C4Value &val) const /* String delegate */ +C4PropertyDelegateStringEditor::C4PropertyDelegateStringEditor(QWidget *parent, bool has_localization_button) + : QWidget(parent), edit(nullptr), localization_button(nullptr), commit_pending(false), text_edited(false) +{ + auto layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setMargin(0); + layout->setSpacing(0); + edit = new QLineEdit(this); + layout->addWidget(edit); + if (has_localization_button) + { + localization_button = new QPushButton(QString(LoadResStr("IDS_CNS_MORE")), this); + layout->addWidget(localization_button); + connect(localization_button, &QPushButton::pressed, this, [this]() { + // Show dialogue + OpenLocalizationDialogue(); + }); + } + connect(edit, &QLineEdit::returnPressed, this, [this]() { + text_edited = true; + commit_pending = true; + emit EditingDoneSignal(); + }); + connect(edit, &QLineEdit::textEdited, this, [this]() { + text_edited = true; + commit_pending = true; + }); +} + +void C4PropertyDelegateStringEditor::OpenLocalizationDialogue() +{ + if (!localization_dialogue) + { + // Make sure we have an updated value + StoreEditedText(); + // Make sure we're using a localized string + if (value.GetType() != C4V_PropList) + { + C4PropList *value_proplist = ::Game.AllocateTranslatedString(); + if (value.GetType() == C4V_String) + { + C4String *lang = ::Strings.RegString(lang_code); + value_proplist->SetPropertyByS(lang, value); + } + value = C4VPropList(value_proplist); + } + // Open dialogue on value + localization_dialogue.reset(new C4ConsoleQtLocalizeStringDlg(::Console.GetState()->window.get(), value)); + connect(localization_dialogue.get(), &C4ConsoleQtLocalizeStringDlg::accepted, this, [this]() { + // Usually, the proplist owned by localization_dialogue is the same as this->value + // However, it may have changed if there was an update call that modified the value while the dialogue was open + // In this case, take the value from the dialogue + SetValue(C4VPropList(localization_dialogue->GetTranslations())); + // Finish editing on the value + CloseLocalizationDialogue(); + commit_pending = true; + emit EditingDoneSignal(); + }); + connect(localization_dialogue.get(), &C4ConsoleQtLocalizeStringDlg::rejected, this, [this]() { + CloseLocalizationDialogue(); + }); + localization_dialogue->show(); + } +} + +void C4PropertyDelegateStringEditor::CloseLocalizationDialogue() +{ + if (localization_dialogue) + { + localization_dialogue->close(); + localization_dialogue.reset(); + } +} + +void C4PropertyDelegateStringEditor::StoreEditedText() +{ + if (text_edited) + { + // TODO: Would be better to handle escaping in the C4Value-to-string code + QString new_value = edit->text(); + new_value = new_value.replace("\\", "\\\\").replace("\"", "\\\""); + C4Value text_value = C4VString(new_value.toUtf8()); + // If translatable, always store as translation proplist + // This makes it easier to collect strings to be localized in the localization overview + if (localization_button) + { + C4PropList *value_proplist = this->value.getPropList(); + if (!value_proplist) + { + value_proplist = ::Game.AllocateTranslatedString(); + } + C4String *lang = ::Strings.RegString(lang_code); + value_proplist->SetPropertyByS(lang, text_value); + } + else + { + this->value = text_value; + } + text_edited = false; + } +} + +void C4PropertyDelegateStringEditor::SetValue(const C4Value &val) +{ + // Set editor text to value + // Resolve text string and default language for localized strings + C4String *s; + C4Value language; + if (localization_button) + { + s = ::Game.GetTranslatedString(val, &language, true); + C4String *language_string = language.getStr(); + SCopy(language_string ? language_string->GetCStr() : Config.General.LanguageEx, lang_code, 2); + localization_button->setText(QString(lang_code)); + } + else + { + s = val.getStr(); + } + edit->setText(QString(s ? s->GetCStr() : "")); + // Remember full value with all localizations + if (val.GetType() == C4V_PropList) + { + if (val != this->value) + { + // Localization proplist: Create a copy (C4Value::Copy() would be nice) + C4PropList *new_value_proplist = new C4PropListScript(); + this->value = C4VPropList(new_value_proplist); + C4PropList *val_proplist = val.getPropList(); + for (C4String *lang : val_proplist->GetSortedLocalProperties()) + { + C4Value lang_string; + val_proplist->GetPropertyByS(lang, &lang_string); + new_value_proplist->SetPropertyByS(lang, lang_string); + } + } + } + else + { + this->value = val; + } +} + +C4Value C4PropertyDelegateStringEditor::GetValue() +{ + // Flush edits from the text field into value + StoreEditedText(); + // Return current value + return this->value; +} + C4PropertyDelegateString::C4PropertyDelegateString(const C4PropertyDelegateFactory *factory, C4PropList *props) : C4PropertyDelegate(factory, props) { -} - -void C4PropertyDelegateString::SetEditorData(QWidget *editor, const C4Value &val, const C4PropertyPath &property_path) const -{ - Editor *line_edit = static_cast(editor); - C4String *s = val.getStr(); - line_edit->setText(QString(s ? s->GetCStr() : "")); -} - -void C4PropertyDelegateString::SetModelData(QObject *editor, const C4PropertyPath &property_path, C4ConsoleQtShape *prop_shape) const -{ - Editor *line_edit = static_cast(editor); - // Only set model data when pressing Enter explicitely; not just when leaving - if (line_edit->commit_pending) + if (props) { - QString new_value = line_edit->text(); - // TODO: Would be better to handle escaping in the C4Value-to-string code - new_value = new_value.replace("\\", "\\\\").replace("\"", "\\\""); - property_path.SetProperty(C4VString(new_value.toUtf8())); + translatable = props->GetPropertyBool(P_Translatable); + } +} + +void C4PropertyDelegateString::SetEditorData(QWidget *aeditor, const C4Value &val, const C4PropertyPath &property_path) const +{ + Editor *editor = static_cast(aeditor); + editor->SetValue(val); +} + +void C4PropertyDelegateString::SetModelData(QObject *aeditor, const C4PropertyPath &property_path, C4ConsoleQtShape *prop_shape) const +{ + Editor *editor = static_cast(aeditor); + // Only set model data when pressing Enter explicitely; not just when leaving + if (editor->IsCommitPending()) + { + property_path.SetProperty(editor->GetValue()); factory->GetPropertyModel()->DoOnUpdateCall(property_path, this); - line_edit->commit_pending = false; + editor->SetCommitPending(false); } } QWidget *C4PropertyDelegateString::CreateEditor(const C4PropertyDelegateFactory *parent_delegate, QWidget *parent, const QStyleOptionViewItem &option, bool by_selection, bool is_child) const { - Editor *editor = new Editor(parent); + Editor *editor = new Editor(parent, translatable); // EditingDone on return or when leaving edit field after a change has been made - connect(editor, &QLineEdit::returnPressed, editor, [this, editor]() { - editor->commit_pending = true; + connect(editor, &Editor::EditingDoneSignal, editor, [this, editor]() { emit EditingDoneSignal(editor); }); - connect(editor, &QLineEdit::textEdited, this, [editor, this]() { - editor->commit_pending = true; - }); // Selection in child enum: Direct focus if (by_selection && is_child) editor->setFocus(); return editor; @@ -358,15 +506,23 @@ QWidget *C4PropertyDelegateString::CreateEditor(const C4PropertyDelegateFactory QString C4PropertyDelegateString::GetDisplayString(const C4Value &v, C4Object *obj, bool short_names) const { // Raw string without "" - C4String *s = v.getStr(); + C4String *s = translatable ? ::Game.GetTranslatedString(v, nullptr, true) : v.getStr(); return QString(s ? s->GetCStr() : ""); } bool C4PropertyDelegateString::IsPasteValid(const C4Value &val) const { - // Check string type - if (val.GetType() != C4V_String) return false; - return true; + // Check string type or translatable proplist + if (val.GetType() == C4V_String) return true; + if (translatable) + { + C4PropList *val_p = val.getPropList(); + if (val_p) + { + return val_p->GetPropertyStr(P_Function) == &::Strings.P[P_Translate]; + } + } + return false; } diff --git a/src/editor/C4ConsoleQtPropListViewer.h b/src/editor/C4ConsoleQtPropListViewer.h index 38c3b0a5b..ea0b2681f 100644 --- a/src/editor/C4ConsoleQtPropListViewer.h +++ b/src/editor/C4ConsoleQtPropListViewer.h @@ -125,15 +125,35 @@ public: bool IsPasteValid(const C4Value &val) const override; }; -class C4PropertyDelegateStringEditor : public QLineEdit +class C4PropertyDelegateStringEditor : public QWidget { + Q_OBJECT +private: + QLineEdit *edit; + QPushButton *localization_button; + bool text_edited, commit_pending; + C4Value value; + char lang_code[3]; + C4Value base_proplist; + std::unique_ptr localization_dialogue; + + void OpenLocalizationDialogue(); + void CloseLocalizationDialogue(); + void StoreEditedText(); public: - C4PropertyDelegateStringEditor(QWidget *parent) : QLineEdit(parent), commit_pending(false) {} - bool commit_pending; + C4PropertyDelegateStringEditor(QWidget *parent, bool has_localization_button); + void SetValue(const C4Value &val); + C4Value GetValue(); + bool IsCommitPending() const { return commit_pending; } + void SetCommitPending(bool to_val) { commit_pending = to_val; } +signals: + void EditingDoneSignal() const; }; class C4PropertyDelegateString : public C4PropertyDelegate { +private: + bool translatable; public: typedef C4PropertyDelegateStringEditor Editor; diff --git a/src/game/C4Game.cpp b/src/game/C4Game.cpp index aed95d6f5..e5292d099 100644 --- a/src/game/C4Game.cpp +++ b/src/game/C4Game.cpp @@ -3890,3 +3890,83 @@ void C4Game::SetGlobalSoundModifier(C4PropList *new_modifier) } ::Application.SoundSystem.Modifiers.SetGlobalModifier(mod, NO_OWNER); } + +C4String *C4Game::GetTranslatedString(const C4Value &input_string, C4Value *selected_language, bool fail_silently) const +{ + // Resolve a localized string + // If a string is passed, just return it + // If a proplist like { DE="Hallo, Welt!", US="Hello, world!" } is passed, return the string matching the selected language + // Nothing? + if (input_string.GetType() == C4V_Nil) + { + return nullptr; + } + // Non-localized string? + if (input_string.GetType() == C4V_String) + { + return input_string._getStr(); + } + // Invalid type for this function? + C4PropList *p = input_string._getPropList(); + if (!p || p->GetPropertyStr(P_Function) != &::Strings.P[P_Translate]) + { + if (fail_silently) + { + return nullptr; + } + else + { + throw C4AulExecError(FormatString("Invalid value for translation: %s", input_string.GetDataString().getData()).getData()); + } + } + // This is a proplist. Resolve the language as the key. + char lang_code[3] = ""; + for (int32_t lang_index = 0; SCopySegment(Config.General.LanguageEx, lang_index, lang_code, ',', 2); ++lang_index) + { + C4String *lang_string = ::Strings.FindString(lang_code); + if (lang_string) // If the string is not found, it cannot be the key in a prop list + { + C4Value localized_string_val; + if (p->GetPropertyByS(lang_string, &localized_string_val)) + { + C4String *localized_string = localized_string_val.getStr(); + if (localized_string) + { + // Found it! + if (selected_language) + { + selected_language->SetString(lang_string); + } + return localized_string; + } + } + } + } + // No language matched. Just use any property and assume it's a language key. + for (C4String *lang_string : p->GetSortedLocalProperties(false)) + { + C4Value localized_string_val; + if (p->GetPropertyByS(lang_string, &localized_string_val)) + { + C4String *localized_string = localized_string_val.getStr(); + if (localized_string) + { + // Found it! + if (selected_language) + { + selected_language->SetString(lang_string); + } + return localized_string; + } + } + } + // No string properties. There's no localized information to be found. + return nullptr; +} + +C4PropList *C4Game::AllocateTranslatedString() +{ + C4PropListScript *value_proplist = new C4PropListScript(); + value_proplist->SetProperty(P_Function, C4VString(&::Strings.P[P_Translate])); + return value_proplist; +} diff --git a/src/game/C4Game.h b/src/game/C4Game.h index 658717062..0aed964a7 100644 --- a/src/game/C4Game.h +++ b/src/game/C4Game.h @@ -290,6 +290,10 @@ public: bool ToggleChart(); // chart dlg on/off void SetGlobalSoundModifier(C4PropList *modifier_props); + // Localized strings in editor props + C4String *GetTranslatedString(const class C4Value &input_string, C4Value *selected_language, bool fail_silently) const; + C4PropList *AllocateTranslatedString(); + static constexpr const char * DirectJoinFilePrefix = "file:"; }; diff --git a/src/game/C4GameScript.cpp b/src/game/C4GameScript.cpp index 6e325e5eb..312c7c99b 100644 --- a/src/game/C4GameScript.cpp +++ b/src/game/C4GameScript.cpp @@ -2742,6 +2742,12 @@ static long FnGetPXSCount(C4PropList * _this, Nillable iMaterial, Nillable } } +static C4String *FnGetTranslatedString(C4PropList * _this, const C4Value & string_data) +{ + // Resolve proplists containing localized strings to the current localization + return ::Game.GetTranslatedString(string_data, nullptr, false); +} + extern C4ScriptConstDef C4ScriptGameConstMap[]; extern C4ScriptFnDef C4ScriptGameFnMap[]; @@ -2953,6 +2959,7 @@ void InitGameFunctionMap(C4AulScriptEngine *pEngine) F(IncinerateLandscape); F(GetGravity); F(SetGravity); + F(GetTranslatedString); #undef F } diff --git a/src/script/C4StringTable.cpp b/src/script/C4StringTable.cpp index f47c77268..3cf20aee8 100644 --- a/src/script/C4StringTable.cpp +++ b/src/script/C4StringTable.cpp @@ -311,6 +311,9 @@ C4StringTable::C4StringTable() P[P_ForceSerialization] = "ForceSerialization"; P[P_DrawArrows] = "DrawArrows"; P[P_SCENPAR] = "SCENPAR"; + P[P_Translatable] = "Translatable"; + P[P_Function] = "Function"; + P[P_Translate] = "Translate"; P[DFA_WALK] = "WALK"; P[DFA_FLIGHT] = "FLIGHT"; P[DFA_KNEEL] = "KNEEL"; diff --git a/src/script/C4StringTable.h b/src/script/C4StringTable.h index f6c143988..7985943d9 100644 --- a/src/script/C4StringTable.h +++ b/src/script/C4StringTable.h @@ -535,6 +535,9 @@ enum C4PropertyName P_ForceSerialization, P_DrawArrows, P_SCENPAR, + P_Translatable, + P_Function, + P_Translate, // Default Action Procedures DFA_WALK, DFA_FLIGHT,