gdscript_translation_parser_plugin.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. /**************************************************************************/
  2. /* gdscript_translation_parser_plugin.cpp */
  3. /**************************************************************************/
  4. /* This file is part of: */
  5. /* GODOT ENGINE */
  6. /* https://godotengine.org */
  7. /**************************************************************************/
  8. /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
  9. /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
  10. /* */
  11. /* Permission is hereby granted, free of charge, to any person obtaining */
  12. /* a copy of this software and associated documentation files (the */
  13. /* "Software"), to deal in the Software without restriction, including */
  14. /* without limitation the rights to use, copy, modify, merge, publish, */
  15. /* distribute, sublicense, and/or sell copies of the Software, and to */
  16. /* permit persons to whom the Software is furnished to do so, subject to */
  17. /* the following conditions: */
  18. /* */
  19. /* The above copyright notice and this permission notice shall be */
  20. /* included in all copies or substantial portions of the Software. */
  21. /* */
  22. /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
  23. /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
  24. /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
  25. /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
  26. /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
  27. /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
  28. /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
  29. /**************************************************************************/
  30. #include "gdscript_translation_parser_plugin.h"
  31. #include "../gdscript.h"
  32. #include "../gdscript_analyzer.h"
  33. #include "core/io/resource_loader.h"
  34. void GDScriptEditorTranslationParserPlugin::get_recognized_extensions(List<String> *r_extensions) const {
  35. GDScriptLanguage::get_singleton()->get_recognized_extensions(r_extensions);
  36. }
  37. Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) {
  38. // Extract all translatable strings using the parsed tree from GDScriptParser.
  39. // The strategy is to find all ExpressionNode and AssignmentNode from the tree and extract strings if relevant, i.e
  40. // Search strings in ExpressionNode -> CallNode -> tr(), set_text(), set_placeholder() etc.
  41. // Search strings in AssignmentNode -> text = "__", tooltip_text = "__" etc.
  42. Error err;
  43. Ref<Resource> loaded_res = ResourceLoader::load(p_path, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err);
  44. ERR_FAIL_COND_V_MSG(err, err, "Failed to load " + p_path);
  45. ids = r_ids;
  46. ids_ctx_plural = r_ids_ctx_plural;
  47. ids_comment.clear();
  48. ids_ctx_plural_comment.clear();
  49. Ref<GDScript> gdscript = loaded_res;
  50. String source_code = gdscript->get_source_code();
  51. GDScriptParser parser;
  52. err = parser.parse(source_code, p_path, false);
  53. ERR_FAIL_COND_V_MSG(err, err, "Failed to parse GDScript with GDScriptParser.");
  54. GDScriptAnalyzer analyzer(&parser);
  55. err = analyzer.analyze();
  56. ERR_FAIL_COND_V_MSG(err, err, "Failed to analyze GDScript with GDScriptAnalyzer.");
  57. comment_data = &parser.comment_data;
  58. // Traverse through the parsed tree from GDScriptParser.
  59. GDScriptParser::ClassNode *c = parser.get_tree();
  60. _traverse_class(c);
  61. comment_data = nullptr;
  62. return OK;
  63. }
  64. void GDScriptEditorTranslationParserPlugin::get_comments(Vector<String> *r_ids_comment, Vector<String> *r_ids_ctx_plural_comment) {
  65. r_ids_comment->append_array(ids_comment);
  66. r_ids_ctx_plural_comment->append_array(ids_ctx_plural_comment);
  67. }
  68. bool GDScriptEditorTranslationParserPlugin::_is_constant_string(const GDScriptParser::ExpressionNode *p_expression) {
  69. ERR_FAIL_NULL_V(p_expression, false);
  70. return p_expression->is_constant && p_expression->reduced_value.is_string();
  71. }
  72. String GDScriptEditorTranslationParserPlugin::_parse_comment(int p_line, bool &r_skip) const {
  73. // Parse inline comment.
  74. if (comment_data->has(p_line)) {
  75. const String stripped_comment = comment_data->get(p_line).comment.trim_prefix("#").strip_edges();
  76. if (stripped_comment.begins_with("TRANSLATORS:")) {
  77. return stripped_comment.trim_prefix("TRANSLATORS:").strip_edges(true, false);
  78. }
  79. if (stripped_comment == "NO_TRANSLATE" || stripped_comment.begins_with("NO_TRANSLATE:")) {
  80. r_skip = true;
  81. return String();
  82. }
  83. }
  84. // Parse multiline comment.
  85. String multiline_comment;
  86. for (int line = p_line - 1; comment_data->has(line) && comment_data->get(line).new_line; line--) {
  87. const String stripped_comment = comment_data->get(line).comment.trim_prefix("#").strip_edges();
  88. if (stripped_comment.is_empty()) {
  89. continue;
  90. }
  91. if (multiline_comment.is_empty()) {
  92. multiline_comment = stripped_comment;
  93. } else {
  94. multiline_comment = stripped_comment + "\n" + multiline_comment;
  95. }
  96. if (stripped_comment.begins_with("TRANSLATORS:")) {
  97. return multiline_comment.trim_prefix("TRANSLATORS:").strip_edges(true, false);
  98. }
  99. if (stripped_comment == "NO_TRANSLATE" || stripped_comment.begins_with("NO_TRANSLATE:")) {
  100. r_skip = true;
  101. return String();
  102. }
  103. }
  104. return String();
  105. }
  106. void GDScriptEditorTranslationParserPlugin::_add_id(const String &p_id, int p_line) {
  107. bool skip = false;
  108. const String comment = _parse_comment(p_line, skip);
  109. if (skip) {
  110. return;
  111. }
  112. ids->push_back(p_id);
  113. ids_comment.push_back(comment);
  114. }
  115. void GDScriptEditorTranslationParserPlugin::_add_id_ctx_plural(const Vector<String> &p_id_ctx_plural, int p_line) {
  116. bool skip = false;
  117. const String comment = _parse_comment(p_line, skip);
  118. if (skip) {
  119. return;
  120. }
  121. ids_ctx_plural->push_back(p_id_ctx_plural);
  122. ids_ctx_plural_comment.push_back(comment);
  123. }
  124. void GDScriptEditorTranslationParserPlugin::_traverse_class(const GDScriptParser::ClassNode *p_class) {
  125. for (int i = 0; i < p_class->members.size(); i++) {
  126. const GDScriptParser::ClassNode::Member &m = p_class->members[i];
  127. // Other member types can't contain translatable strings.
  128. switch (m.type) {
  129. case GDScriptParser::ClassNode::Member::CLASS:
  130. _traverse_class(m.m_class);
  131. break;
  132. case GDScriptParser::ClassNode::Member::FUNCTION:
  133. _traverse_function(m.function);
  134. break;
  135. case GDScriptParser::ClassNode::Member::VARIABLE:
  136. _assess_expression(m.variable->initializer);
  137. if (m.variable->property == GDScriptParser::VariableNode::PROP_INLINE) {
  138. _traverse_function(m.variable->setter);
  139. _traverse_function(m.variable->getter);
  140. }
  141. break;
  142. default:
  143. break;
  144. }
  145. }
  146. }
  147. void GDScriptEditorTranslationParserPlugin::_traverse_function(const GDScriptParser::FunctionNode *p_func) {
  148. if (!p_func) {
  149. return;
  150. }
  151. for (int i = 0; i < p_func->parameters.size(); i++) {
  152. _assess_expression(p_func->parameters[i]->initializer);
  153. }
  154. _traverse_block(p_func->body);
  155. }
  156. void GDScriptEditorTranslationParserPlugin::_traverse_block(const GDScriptParser::SuiteNode *p_suite) {
  157. if (!p_suite) {
  158. return;
  159. }
  160. const Vector<GDScriptParser::Node *> &statements = p_suite->statements;
  161. for (int i = 0; i < statements.size(); i++) {
  162. const GDScriptParser::Node *statement = statements[i];
  163. // BREAK, BREAKPOINT, CONSTANT, CONTINUE, and PASS are skipped because they can't contain translatable strings.
  164. switch (statement->type) {
  165. case GDScriptParser::Node::ASSERT: {
  166. const GDScriptParser::AssertNode *assert_node = static_cast<const GDScriptParser::AssertNode *>(statement);
  167. _assess_expression(assert_node->condition);
  168. _assess_expression(assert_node->message);
  169. } break;
  170. case GDScriptParser::Node::ASSIGNMENT: {
  171. _assess_assignment(static_cast<const GDScriptParser::AssignmentNode *>(statement));
  172. } break;
  173. case GDScriptParser::Node::FOR: {
  174. const GDScriptParser::ForNode *for_node = static_cast<const GDScriptParser::ForNode *>(statement);
  175. _assess_expression(for_node->list);
  176. _traverse_block(for_node->loop);
  177. } break;
  178. case GDScriptParser::Node::IF: {
  179. const GDScriptParser::IfNode *if_node = static_cast<const GDScriptParser::IfNode *>(statement);
  180. _assess_expression(if_node->condition);
  181. _traverse_block(if_node->true_block);
  182. _traverse_block(if_node->false_block);
  183. } break;
  184. case GDScriptParser::Node::MATCH: {
  185. const GDScriptParser::MatchNode *match_node = static_cast<const GDScriptParser::MatchNode *>(statement);
  186. _assess_expression(match_node->test);
  187. for (int j = 0; j < match_node->branches.size(); j++) {
  188. _traverse_block(match_node->branches[j]->guard_body);
  189. _traverse_block(match_node->branches[j]->block);
  190. }
  191. } break;
  192. case GDScriptParser::Node::RETURN: {
  193. _assess_expression(static_cast<const GDScriptParser::ReturnNode *>(statement)->return_value);
  194. } break;
  195. case GDScriptParser::Node::VARIABLE: {
  196. _assess_expression(static_cast<const GDScriptParser::VariableNode *>(statement)->initializer);
  197. } break;
  198. case GDScriptParser::Node::WHILE: {
  199. const GDScriptParser::WhileNode *while_node = static_cast<const GDScriptParser::WhileNode *>(statement);
  200. _assess_expression(while_node->condition);
  201. _traverse_block(while_node->loop);
  202. } break;
  203. default: {
  204. if (statement->is_expression()) {
  205. _assess_expression(static_cast<const GDScriptParser::ExpressionNode *>(statement));
  206. }
  207. } break;
  208. }
  209. }
  210. }
  211. void GDScriptEditorTranslationParserPlugin::_assess_expression(const GDScriptParser::ExpressionNode *p_expression) {
  212. // Explore all ExpressionNodes to find CallNodes which contain translation strings, such as tr(), set_text() etc.
  213. // tr() can be embedded quite deep within multiple ExpressionNodes so need to dig down to search through all ExpressionNodes.
  214. if (!p_expression) {
  215. return;
  216. }
  217. // GET_NODE, IDENTIFIER, LITERAL, PRELOAD, SELF, and TYPE are skipped because they can't contain translatable strings.
  218. switch (p_expression->type) {
  219. case GDScriptParser::Node::ARRAY: {
  220. const GDScriptParser::ArrayNode *array_node = static_cast<const GDScriptParser::ArrayNode *>(p_expression);
  221. for (int i = 0; i < array_node->elements.size(); i++) {
  222. _assess_expression(array_node->elements[i]);
  223. }
  224. } break;
  225. case GDScriptParser::Node::ASSIGNMENT: {
  226. _assess_assignment(static_cast<const GDScriptParser::AssignmentNode *>(p_expression));
  227. } break;
  228. case GDScriptParser::Node::AWAIT: {
  229. _assess_expression(static_cast<const GDScriptParser::AwaitNode *>(p_expression)->to_await);
  230. } break;
  231. case GDScriptParser::Node::BINARY_OPERATOR: {
  232. const GDScriptParser::BinaryOpNode *binary_op_node = static_cast<const GDScriptParser::BinaryOpNode *>(p_expression);
  233. _assess_expression(binary_op_node->left_operand);
  234. _assess_expression(binary_op_node->right_operand);
  235. } break;
  236. case GDScriptParser::Node::CALL: {
  237. _assess_call(static_cast<const GDScriptParser::CallNode *>(p_expression));
  238. } break;
  239. case GDScriptParser::Node::CAST: {
  240. _assess_expression(static_cast<const GDScriptParser::CastNode *>(p_expression)->operand);
  241. } break;
  242. case GDScriptParser::Node::DICTIONARY: {
  243. const GDScriptParser::DictionaryNode *dict_node = static_cast<const GDScriptParser::DictionaryNode *>(p_expression);
  244. for (int i = 0; i < dict_node->elements.size(); i++) {
  245. _assess_expression(dict_node->elements[i].key);
  246. _assess_expression(dict_node->elements[i].value);
  247. }
  248. } break;
  249. case GDScriptParser::Node::LAMBDA: {
  250. _traverse_function(static_cast<const GDScriptParser::LambdaNode *>(p_expression)->function);
  251. } break;
  252. case GDScriptParser::Node::SUBSCRIPT: {
  253. const GDScriptParser::SubscriptNode *subscript_node = static_cast<const GDScriptParser::SubscriptNode *>(p_expression);
  254. _assess_expression(subscript_node->base);
  255. if (!subscript_node->is_attribute) {
  256. _assess_expression(subscript_node->index);
  257. }
  258. } break;
  259. case GDScriptParser::Node::TERNARY_OPERATOR: {
  260. const GDScriptParser::TernaryOpNode *ternary_op_node = static_cast<const GDScriptParser::TernaryOpNode *>(p_expression);
  261. _assess_expression(ternary_op_node->condition);
  262. _assess_expression(ternary_op_node->true_expr);
  263. _assess_expression(ternary_op_node->false_expr);
  264. } break;
  265. case GDScriptParser::Node::TYPE_TEST: {
  266. _assess_expression(static_cast<const GDScriptParser::TypeTestNode *>(p_expression)->operand);
  267. } break;
  268. case GDScriptParser::Node::UNARY_OPERATOR: {
  269. _assess_expression(static_cast<const GDScriptParser::UnaryOpNode *>(p_expression)->operand);
  270. } break;
  271. default: {
  272. } break;
  273. }
  274. }
  275. void GDScriptEditorTranslationParserPlugin::_assess_assignment(const GDScriptParser::AssignmentNode *p_assignment) {
  276. _assess_expression(p_assignment->assignee);
  277. _assess_expression(p_assignment->assigned_value);
  278. // Extract the translatable strings coming from assignments. For example, get_node("Label").text = "____"
  279. StringName assignee_name;
  280. if (p_assignment->assignee->type == GDScriptParser::Node::IDENTIFIER) {
  281. assignee_name = static_cast<const GDScriptParser::IdentifierNode *>(p_assignment->assignee)->name;
  282. } else if (p_assignment->assignee->type == GDScriptParser::Node::SUBSCRIPT) {
  283. const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(p_assignment->assignee);
  284. if (subscript->is_attribute && subscript->attribute) {
  285. assignee_name = subscript->attribute->name;
  286. } else if (subscript->index && _is_constant_string(subscript->index)) {
  287. assignee_name = subscript->index->reduced_value;
  288. }
  289. }
  290. if (assignee_name != StringName() && assignment_patterns.has(assignee_name) && _is_constant_string(p_assignment->assigned_value)) {
  291. // 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.
  292. _add_id(p_assignment->assigned_value->reduced_value, p_assignment->assigned_value->start_line);
  293. } else if (assignee_name == fd_filters) {
  294. // Extract from `get_node("FileDialog").filters = <filter array>`.
  295. _extract_fd_filter_array(p_assignment->assigned_value);
  296. }
  297. }
  298. void GDScriptEditorTranslationParserPlugin::_assess_call(const GDScriptParser::CallNode *p_call) {
  299. _assess_expression(p_call->callee);
  300. for (int i = 0; i < p_call->arguments.size(); i++) {
  301. _assess_expression(p_call->arguments[i]);
  302. }
  303. // Extract the translatable strings coming from function calls. For example:
  304. // tr("___"), get_node("Label").set_text("____"), get_node("LineEdit").set_placeholder("____").
  305. StringName function_name = p_call->function_name;
  306. // Variables for extracting tr() and tr_n().
  307. Vector<String> id_ctx_plural;
  308. id_ctx_plural.resize(3);
  309. bool extract_id_ctx_plural = true;
  310. if (function_name == tr_func || function_name == atr_func) {
  311. // Extract from `tr(id, ctx)` or `atr(id, ctx)`.
  312. for (int i = 0; i < p_call->arguments.size(); i++) {
  313. if (_is_constant_string(p_call->arguments[i])) {
  314. id_ctx_plural.write[i] = p_call->arguments[i]->reduced_value;
  315. } else {
  316. // Avoid adding something like tr("Flying dragon", var_context_level_1). We want to extract both id and context together.
  317. extract_id_ctx_plural = false;
  318. }
  319. }
  320. if (extract_id_ctx_plural) {
  321. _add_id_ctx_plural(id_ctx_plural, p_call->start_line);
  322. }
  323. } else if (function_name == trn_func || function_name == atrn_func) {
  324. // Extract from `tr_n(id, plural, n, ctx)` or `atr_n(id, plural, n, ctx)`.
  325. Vector<int> indices;
  326. indices.push_back(0);
  327. indices.push_back(3);
  328. indices.push_back(1);
  329. for (int i = 0; i < indices.size(); i++) {
  330. if (indices[i] >= p_call->arguments.size()) {
  331. continue;
  332. }
  333. if (_is_constant_string(p_call->arguments[indices[i]])) {
  334. id_ctx_plural.write[i] = p_call->arguments[indices[i]]->reduced_value;
  335. } else {
  336. extract_id_ctx_plural = false;
  337. }
  338. }
  339. if (extract_id_ctx_plural) {
  340. _add_id_ctx_plural(id_ctx_plural, p_call->start_line);
  341. }
  342. } else if (first_arg_patterns.has(function_name)) {
  343. if (!p_call->arguments.is_empty() && _is_constant_string(p_call->arguments[0])) {
  344. _add_id(p_call->arguments[0]->reduced_value, p_call->arguments[0]->start_line);
  345. }
  346. } else if (second_arg_patterns.has(function_name)) {
  347. if (p_call->arguments.size() > 1 && _is_constant_string(p_call->arguments[1])) {
  348. _add_id(p_call->arguments[1]->reduced_value, p_call->arguments[1]->start_line);
  349. }
  350. } else if (function_name == fd_add_filter) {
  351. // Extract the 'JPE Images' in this example - get_node("FileDialog").add_filter("*.jpg; JPE Images").
  352. if (!p_call->arguments.is_empty()) {
  353. _extract_fd_filter_string(p_call->arguments[0], p_call->arguments[0]->start_line);
  354. }
  355. } else if (function_name == fd_set_filter) {
  356. // Extract from `get_node("FileDialog").set_filters(<filter array>)`.
  357. if (!p_call->arguments.is_empty()) {
  358. _extract_fd_filter_array(p_call->arguments[0]);
  359. }
  360. }
  361. }
  362. void GDScriptEditorTranslationParserPlugin::_extract_fd_filter_string(const GDScriptParser::ExpressionNode *p_expression, int p_line) {
  363. // Extract the name in "extension ; name".
  364. if (_is_constant_string(p_expression)) {
  365. PackedStringArray arr = p_expression->reduced_value.operator String().split(";", true);
  366. ERR_FAIL_COND_MSG(arr.size() != 2, "Argument for setting FileDialog has bad format.");
  367. _add_id(arr[1].strip_edges(), p_line);
  368. }
  369. }
  370. void GDScriptEditorTranslationParserPlugin::_extract_fd_filter_array(const GDScriptParser::ExpressionNode *p_expression) {
  371. const GDScriptParser::ArrayNode *array_node = nullptr;
  372. if (p_expression->type == GDScriptParser::Node::ARRAY) {
  373. // Extract from `["*.png ; PNG Images","*.gd ; GDScript Files"]` (implicit cast to `PackedStringArray`).
  374. array_node = static_cast<const GDScriptParser::ArrayNode *>(p_expression);
  375. } else if (p_expression->type == GDScriptParser::Node::CALL) {
  376. // Extract from `PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"])`.
  377. const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(p_expression);
  378. if (call_node->get_callee_type() == GDScriptParser::Node::IDENTIFIER && call_node->function_name == SNAME("PackedStringArray") && !call_node->arguments.is_empty() && call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) {
  379. array_node = static_cast<const GDScriptParser::ArrayNode *>(call_node->arguments[0]);
  380. }
  381. }
  382. if (array_node) {
  383. for (int i = 0; i < array_node->elements.size(); i++) {
  384. _extract_fd_filter_string(array_node->elements[i], array_node->elements[i]->start_line);
  385. }
  386. }
  387. }
  388. GDScriptEditorTranslationParserPlugin::GDScriptEditorTranslationParserPlugin() {
  389. assignment_patterns.insert("text");
  390. assignment_patterns.insert("placeholder_text");
  391. assignment_patterns.insert("tooltip_text");
  392. first_arg_patterns.insert("set_text");
  393. first_arg_patterns.insert("set_tooltip_text");
  394. first_arg_patterns.insert("set_placeholder");
  395. first_arg_patterns.insert("add_tab");
  396. first_arg_patterns.insert("add_check_item");
  397. first_arg_patterns.insert("add_item");
  398. first_arg_patterns.insert("add_multistate_item");
  399. first_arg_patterns.insert("add_radio_check_item");
  400. first_arg_patterns.insert("add_separator");
  401. first_arg_patterns.insert("add_submenu_item");
  402. second_arg_patterns.insert("set_tab_title");
  403. second_arg_patterns.insert("add_icon_check_item");
  404. second_arg_patterns.insert("add_icon_item");
  405. second_arg_patterns.insert("add_icon_radio_check_item");
  406. second_arg_patterns.insert("set_item_text");
  407. }