Skip to content
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

GDScript: Add constant string support for POT generator #80020

Merged
merged 1 commit into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 56 additions & 51 deletions modules/gdscript/editor/gdscript_translation_parser_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "gdscript_translation_parser_plugin.h"

#include "../gdscript.h"
#include "../gdscript_analyzer.h"

#include "core/io/resource_loader.h"

Expand Down Expand Up @@ -58,10 +59,11 @@ Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Ve

GDScriptParser parser;
err = parser.parse(source_code, p_path, false);
if (err != OK) {
ERR_PRINT("Failed to parse with GDScript with GDScriptParser.");
return err;
}
ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to parse GDScript with GDScriptParser.");

GDScriptAnalyzer analyzer(&parser);
err = analyzer.analyze();
ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to analyze GDScript with GDScriptAnalyzer.");

// Traverse through the parsed tree from GDScriptParser.
GDScriptParser::ClassNode *c = parser.get_tree();
Expand All @@ -70,6 +72,11 @@ Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Ve
return OK;
}

bool GDScriptEditorTranslationParserPlugin::_is_constant_string(const GDScriptParser::ExpressionNode *p_expression) {
ERR_FAIL_NULL_V(p_expression, false);
return p_expression->is_constant && (p_expression->reduced_value.get_type() == Variant::STRING || p_expression->reduced_value.get_type() == Variant::STRING_NAME);
}

void GDScriptEditorTranslationParserPlugin::_traverse_class(const GDScriptParser::ClassNode *p_class) {
for (int i = 0; i < p_class->members.size(); i++) {
const GDScriptParser::ClassNode::Member &m = p_class->members[i];
Expand Down Expand Up @@ -105,60 +112,60 @@ void GDScriptEditorTranslationParserPlugin::_traverse_block(const GDScriptParser

const Vector<GDScriptParser::Node *> &statements = p_suite->statements;
for (int i = 0; i < statements.size(); i++) {
GDScriptParser::Node *statement = statements[i];
const GDScriptParser::Node *statement = statements[i];

// Statements with Node type constant, break, continue, pass, breakpoint are skipped because they can't contain translatable strings.
switch (statement->type) {
case GDScriptParser::Node::VARIABLE:
_assess_expression(static_cast<GDScriptParser::VariableNode *>(statement)->initializer);
_assess_expression(static_cast<const GDScriptParser::VariableNode *>(statement)->initializer);
break;
case GDScriptParser::Node::IF: {
GDScriptParser::IfNode *if_node = static_cast<GDScriptParser::IfNode *>(statement);
const GDScriptParser::IfNode *if_node = static_cast<const GDScriptParser::IfNode *>(statement);
_assess_expression(if_node->condition);
//FIXME : if the elif logic is changed in GDScriptParser, then this probably will have to change as well. See GDScriptParser::TreePrinter::print_if().
_traverse_block(if_node->true_block);
_traverse_block(if_node->false_block);
break;
}
case GDScriptParser::Node::FOR: {
GDScriptParser::ForNode *for_node = static_cast<GDScriptParser::ForNode *>(statement);
const GDScriptParser::ForNode *for_node = static_cast<const GDScriptParser::ForNode *>(statement);
_assess_expression(for_node->list);
_traverse_block(for_node->loop);
break;
}
case GDScriptParser::Node::WHILE: {
GDScriptParser::WhileNode *while_node = static_cast<GDScriptParser::WhileNode *>(statement);
const GDScriptParser::WhileNode *while_node = static_cast<const GDScriptParser::WhileNode *>(statement);
_assess_expression(while_node->condition);
_traverse_block(while_node->loop);
break;
}
case GDScriptParser::Node::MATCH: {
GDScriptParser::MatchNode *match_node = static_cast<GDScriptParser::MatchNode *>(statement);
const GDScriptParser::MatchNode *match_node = static_cast<const GDScriptParser::MatchNode *>(statement);
_assess_expression(match_node->test);
for (int j = 0; j < match_node->branches.size(); j++) {
_traverse_block(match_node->branches[j]->block);
}
break;
}
case GDScriptParser::Node::RETURN:
_assess_expression(static_cast<GDScriptParser::ReturnNode *>(statement)->return_value);
_assess_expression(static_cast<const GDScriptParser::ReturnNode *>(statement)->return_value);
break;
case GDScriptParser::Node::ASSERT:
_assess_expression((static_cast<GDScriptParser::AssertNode *>(statement))->condition);
_assess_expression((static_cast<const GDScriptParser::AssertNode *>(statement))->condition);
break;
case GDScriptParser::Node::ASSIGNMENT:
_assess_assignment(static_cast<GDScriptParser::AssignmentNode *>(statement));
_assess_assignment(static_cast<const GDScriptParser::AssignmentNode *>(statement));
break;
default:
if (statement->is_expression()) {
_assess_expression(static_cast<GDScriptParser::ExpressionNode *>(statement));
_assess_expression(static_cast<const GDScriptParser::ExpressionNode *>(statement));
}
break;
}
}
}

void GDScriptEditorTranslationParserPlugin::_assess_expression(GDScriptParser::ExpressionNode *p_expression) {
void GDScriptEditorTranslationParserPlugin::_assess_expression(const GDScriptParser::ExpressionNode *p_expression) {
// Explore all ExpressionNodes to find CallNodes which contain translation strings, such as tr(), set_text() etc.
// tr() can be embedded quite deep within multiple ExpressionNodes so need to dig down to search through all ExpressionNodes.
if (!p_expression) {
Expand All @@ -169,38 +176,38 @@ void GDScriptEditorTranslationParserPlugin::_assess_expression(GDScriptParser::E
// containing translation strings.
switch (p_expression->type) {
case GDScriptParser::Node::ARRAY: {
GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(p_expression);
const GDScriptParser::ArrayNode *array_node = static_cast<const GDScriptParser::ArrayNode *>(p_expression);
for (int i = 0; i < array_node->elements.size(); i++) {
_assess_expression(array_node->elements[i]);
}
break;
}
case GDScriptParser::Node::ASSIGNMENT:
_assess_assignment(static_cast<GDScriptParser::AssignmentNode *>(p_expression));
_assess_assignment(static_cast<const GDScriptParser::AssignmentNode *>(p_expression));
break;
case GDScriptParser::Node::BINARY_OPERATOR: {
GDScriptParser::BinaryOpNode *binary_op_node = static_cast<GDScriptParser::BinaryOpNode *>(p_expression);
const GDScriptParser::BinaryOpNode *binary_op_node = static_cast<const GDScriptParser::BinaryOpNode *>(p_expression);
_assess_expression(binary_op_node->left_operand);
_assess_expression(binary_op_node->right_operand);
break;
}
case GDScriptParser::Node::CALL: {
GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_expression);
const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(p_expression);
_extract_from_call(call_node);
for (int i = 0; i < call_node->arguments.size(); i++) {
_assess_expression(call_node->arguments[i]);
}
} break;
case GDScriptParser::Node::DICTIONARY: {
GDScriptParser::DictionaryNode *dict_node = static_cast<GDScriptParser::DictionaryNode *>(p_expression);
const GDScriptParser::DictionaryNode *dict_node = static_cast<const GDScriptParser::DictionaryNode *>(p_expression);
for (int i = 0; i < dict_node->elements.size(); i++) {
_assess_expression(dict_node->elements[i].key);
_assess_expression(dict_node->elements[i].value);
}
break;
}
case GDScriptParser::Node::TERNARY_OPERATOR: {
GDScriptParser::TernaryOpNode *ternary_op_node = static_cast<GDScriptParser::TernaryOpNode *>(p_expression);
const GDScriptParser::TernaryOpNode *ternary_op_node = static_cast<const GDScriptParser::TernaryOpNode *>(p_expression);
_assess_expression(ternary_op_node->condition);
_assess_expression(ternary_op_node->true_expr);
_assess_expression(ternary_op_node->false_expr);
Expand All @@ -211,39 +218,39 @@ void GDScriptEditorTranslationParserPlugin::_assess_expression(GDScriptParser::E
}
}

void GDScriptEditorTranslationParserPlugin::_assess_assignment(GDScriptParser::AssignmentNode *p_assignment) {
void GDScriptEditorTranslationParserPlugin::_assess_assignment(const GDScriptParser::AssignmentNode *p_assignment) {
// Extract the translatable strings coming from assignments. For example, get_node("Label").text = "____"

StringName assignee_name;
if (p_assignment->assignee->type == GDScriptParser::Node::IDENTIFIER) {
assignee_name = static_cast<GDScriptParser::IdentifierNode *>(p_assignment->assignee)->name;
assignee_name = static_cast<const GDScriptParser::IdentifierNode *>(p_assignment->assignee)->name;
} else if (p_assignment->assignee->type == GDScriptParser::Node::SUBSCRIPT) {
assignee_name = static_cast<GDScriptParser::SubscriptNode *>(p_assignment->assignee)->attribute->name;
assignee_name = static_cast<const GDScriptParser::SubscriptNode *>(p_assignment->assignee)->attribute->name;
}

if (assignment_patterns.has(assignee_name) && p_assignment->assigned_value->type == GDScriptParser::Node::LITERAL) {
// If the assignment is towards one of the extract patterns (text, tooltip_text etc.), and the value is a string literal, we collect the string.
ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_assignment->assigned_value)->value);
if (assignment_patterns.has(assignee_name) && _is_constant_string(p_assignment->assigned_value)) {
// If the assignment is towards one of the extract patterns (text, tooltip_text etc.), and the value is a constant string, we collect the string.
ids->push_back(p_assignment->assigned_value->reduced_value);
} else if (assignee_name == fd_filters && p_assignment->assigned_value->type == GDScriptParser::Node::CALL) {
// FileDialog.filters accepts assignment in the form of PackedStringArray. For example,
// get_node("FileDialog").filters = PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"]).

GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_assignment->assigned_value);
const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(p_assignment->assigned_value);
if (call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) {
GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(call_node->arguments[0]);
const GDScriptParser::ArrayNode *array_node = static_cast<const GDScriptParser::ArrayNode *>(call_node->arguments[0]);

// Extract the name in "extension ; name" of PackedStringArray.
for (int i = 0; i < array_node->elements.size(); i++) {
_extract_fd_literals(array_node->elements[i]);
_extract_fd_constant_strings(array_node->elements[i]);
}
}
} else {
// If the assignee is not in extract patterns or the assigned_value is not Literal type, try to see if the assigned_value contains tr().
// If the assignee is not in extract patterns or the assigned_value is not a constant string, try to see if the assigned_value contains tr().
_assess_expression(p_assignment->assigned_value);
}
}

void GDScriptEditorTranslationParserPlugin::_extract_from_call(GDScriptParser::CallNode *p_call) {
void GDScriptEditorTranslationParserPlugin::_extract_from_call(const GDScriptParser::CallNode *p_call) {
// Extract the translatable strings coming from function calls. For example:
// tr("___"), get_node("Label").set_text("____"), get_node("LineEdit").set_placeholder("____").

Expand All @@ -257,8 +264,8 @@ void GDScriptEditorTranslationParserPlugin::_extract_from_call(GDScriptParser::C
if (function_name == tr_func) {
// Extract from tr(id, ctx).
for (int i = 0; i < p_call->arguments.size(); i++) {
if (p_call->arguments[i]->type == GDScriptParser::Node::LITERAL) {
id_ctx_plural.write[i] = static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[i])->value;
if (_is_constant_string(p_call->arguments[i])) {
id_ctx_plural.write[i] = p_call->arguments[i]->reduced_value;
} else {
// Avoid adding something like tr("Flying dragon", var_context_level_1). We want to extract both id and context together.
extract_id_ctx_plural = false;
Expand All @@ -278,8 +285,8 @@ void GDScriptEditorTranslationParserPlugin::_extract_from_call(GDScriptParser::C
continue;
}

if (p_call->arguments[indices[i]]->type == GDScriptParser::Node::LITERAL) {
id_ctx_plural.write[i] = static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[indices[i]])->value;
if (_is_constant_string(p_call->arguments[indices[i]])) {
id_ctx_plural.write[i] = p_call->arguments[indices[i]]->reduced_value;
} else {
extract_id_ctx_plural = false;
}
Expand All @@ -288,45 +295,43 @@ void GDScriptEditorTranslationParserPlugin::_extract_from_call(GDScriptParser::C
ids_ctx_plural->push_back(id_ctx_plural);
}
} else if (first_arg_patterns.has(function_name)) {
// Extracting argument with only string literals. In other words, not extracting something like set_text("hello " + some_var).
if (p_call->arguments[0]->type == GDScriptParser::Node::LITERAL) {
ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[0])->value);
if (_is_constant_string(p_call->arguments[0])) {
ids->push_back(p_call->arguments[0]->reduced_value);
}
} else if (second_arg_patterns.has(function_name)) {
if (p_call->arguments[1]->type == GDScriptParser::Node::LITERAL) {
ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[1])->value);
if (_is_constant_string(p_call->arguments[1])) {
ids->push_back(p_call->arguments[1]->reduced_value);
}
} else if (function_name == fd_add_filter) {
// Extract the 'JPE Images' in this example - get_node("FileDialog").add_filter("*.jpg; JPE Images").
_extract_fd_literals(p_call->arguments[0]);

_extract_fd_constant_strings(p_call->arguments[0]);
} else if (function_name == fd_set_filter && p_call->arguments[0]->type == GDScriptParser::Node::CALL) {
// FileDialog.set_filters() accepts assignment in the form of PackedStringArray. For example,
// get_node("FileDialog").set_filters( PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"])).

GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_call->arguments[0]);
const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(p_call->arguments[0]);
if (call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) {
GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(call_node->arguments[0]);
const GDScriptParser::ArrayNode *array_node = static_cast<const GDScriptParser::ArrayNode *>(call_node->arguments[0]);
for (int i = 0; i < array_node->elements.size(); i++) {
_extract_fd_literals(array_node->elements[i]);
_extract_fd_constant_strings(array_node->elements[i]);
}
}
}

if (p_call->callee && p_call->callee->type == GDScriptParser::Node::SUBSCRIPT) {
GDScriptParser::SubscriptNode *subscript_node = static_cast<GDScriptParser::SubscriptNode *>(p_call->callee);
const GDScriptParser::SubscriptNode *subscript_node = static_cast<const GDScriptParser::SubscriptNode *>(p_call->callee);
if (subscript_node->base && subscript_node->base->type == GDScriptParser::Node::CALL) {
GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(subscript_node->base);
const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(subscript_node->base);
_extract_from_call(call_node);
}
}
}

void GDScriptEditorTranslationParserPlugin::_extract_fd_literals(GDScriptParser::ExpressionNode *p_expression) {
void GDScriptEditorTranslationParserPlugin::_extract_fd_constant_strings(const GDScriptParser::ExpressionNode *p_expression) {
// Extract the name in "extension ; name".

if (p_expression->type == GDScriptParser::Node::LITERAL) {
String arg_val = String(static_cast<GDScriptParser::LiteralNode *>(p_expression)->value);
if (_is_constant_string(p_expression)) {
String arg_val = p_expression->reduced_value;
PackedStringArray arr = arg_val.split(";", true);
if (arr.size() != 2) {
ERR_PRINT("Argument for setting FileDialog has bad format.");
Expand Down
10 changes: 6 additions & 4 deletions modules/gdscript/editor/gdscript_translation_parser_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@ class GDScriptEditorTranslationParserPlugin : public EditorTranslationParserPlug
StringName fd_set_filter = "set_filters";
StringName fd_filters = "filters";

static bool _is_constant_string(const GDScriptParser::ExpressionNode *p_expression);

void _traverse_class(const GDScriptParser::ClassNode *p_class);
void _traverse_function(const GDScriptParser::FunctionNode *p_func);
void _traverse_block(const GDScriptParser::SuiteNode *p_suite);

void _read_variable(const GDScriptParser::VariableNode *p_var);
void _assess_expression(GDScriptParser::ExpressionNode *p_expression);
void _assess_assignment(GDScriptParser::AssignmentNode *p_assignment);
void _extract_from_call(GDScriptParser::CallNode *p_call);
void _extract_fd_literals(GDScriptParser::ExpressionNode *p_expression);
void _assess_expression(const GDScriptParser::ExpressionNode *p_expression);
void _assess_assignment(const GDScriptParser::AssignmentNode *p_assignment);
void _extract_from_call(const GDScriptParser::CallNode *p_call);
void _extract_fd_constant_strings(const GDScriptParser::ExpressionNode *p_expression);

public:
virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) override;
Expand Down