mirror of
https://github.com/godotengine/godot.git
synced 2025-10-19 07:53:26 +00:00
Compare commits
16 commits
36b92128b1
...
20430236e7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
20430236e7 | ||
![]() |
49219de402 | ||
![]() |
1ae616d7d8 | ||
![]() |
ca524532e6 | ||
![]() |
57e041d357 | ||
![]() |
71031e4adb | ||
![]() |
710a6e0303 | ||
![]() |
a281e91c5a | ||
![]() |
b49aea89d0 | ||
![]() |
1e8fd575a9 | ||
![]() |
80ce18918e | ||
![]() |
a63dd0704f | ||
![]() |
4e80190a46 | ||
![]() |
e882e42e1b | ||
![]() |
ebb96e2303 | ||
![]() |
3f03260a21 |
42 changed files with 1065 additions and 853 deletions
|
@ -31,7 +31,7 @@
|
|||
#include "translation_loader_po.h"
|
||||
|
||||
#include "core/io/file_access.h"
|
||||
#include "core/string/translation_po.h"
|
||||
#include "core/string/translation.h"
|
||||
|
||||
Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_error) {
|
||||
if (r_error) {
|
||||
|
@ -39,7 +39,8 @@ Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_
|
|||
}
|
||||
|
||||
const String path = f->get_path();
|
||||
Ref<TranslationPO> translation = Ref<TranslationPO>(memnew(TranslationPO));
|
||||
Ref<Translation> translation;
|
||||
translation.instantiate();
|
||||
String config;
|
||||
|
||||
uint32_t magic = f->get_32();
|
||||
|
@ -112,7 +113,7 @@ Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_
|
|||
int p_start = config.find("Plural-Forms");
|
||||
if (p_start != -1) {
|
||||
int p_end = config.find_char('\n', p_start);
|
||||
translation->set_plural_rule(config.substr(p_start, p_end - p_start));
|
||||
translation->set_plural_rules_override(config.substr(p_start, p_end - p_start));
|
||||
}
|
||||
} else {
|
||||
uint32_t str_start = 0;
|
||||
|
@ -228,8 +229,8 @@ Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_
|
|||
int p_start = config.find("Plural-Forms");
|
||||
if (p_start != -1) {
|
||||
int p_end = config.find_char('\n', p_start);
|
||||
translation->set_plural_rule(config.substr(p_start, p_end - p_start));
|
||||
plural_forms = translation->get_plural_forms();
|
||||
translation->set_plural_rules_override(config.substr(p_start, p_end - p_start));
|
||||
plural_forms = translation->get_nplurals();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
|
||||
#include "core/io/file_access.h"
|
||||
#include "core/io/resource_loader.h"
|
||||
#include "core/string/translation.h"
|
||||
|
||||
class TranslationLoaderPO : public ResourceFormatLoader {
|
||||
GDSOFTCLASS(TranslationLoaderPO, ResourceFormatLoader);
|
||||
|
|
|
@ -1194,3 +1194,50 @@ static const char *script_list[][2] = {
|
|||
{ "Zanabazar Square", "Zanb" },
|
||||
{ nullptr, nullptr }
|
||||
};
|
||||
|
||||
// Plural rules.
|
||||
// Reference:
|
||||
// - https://github.com/unicode-org/cldr/blob/main/common/supplemental/plurals.xml
|
||||
static const char *plural_rules[][2] = {
|
||||
{ "bm bo dz hnj id ig ii in ja jbo jv jw kde kea km ko lkt lo ms my nqo osa root sah ses sg su th to tpi vi wo yo yue zh", "nplurals=1; plural=0;" },
|
||||
{ "am as bn doi fa gu hi kn pcm zu", "nplurals=2; plural=(n==0 || n==1);" },
|
||||
{ "ff hy kab", "nplurals=2; plural=(n > 1);" },
|
||||
{ "ast de en et fi fy gl ia io ji lij nl sc sv sw ur yi", "nplurals=2; plural=(n != 1);" },
|
||||
{ "si", "nplurals=2; plural=(n > 1);" },
|
||||
{ "ak bho csw guw ln mg nso pa ti wa", "nplurals=2; plural=(n > 1);" },
|
||||
{ "tzm", "nplurals=2; plural=(n<=1 || (n>=11 && n<=99));" },
|
||||
{ "af an asa az bal bem bez bg brx ce cgg chr ckb dv ee el eo eu fo fur gsw ha haw hu jgo jmc ka kaj kcg kk kkj kl ks ksb ku ky lb lg mas mgo ml mn mr nah nb nd ne nn nnh no nr ny nyn om or os pap ps rm rof rwk saq sd sdh seh sn so sq ss ssy st syr ta te teo tig tk tn tr ts ug uz ve vo vun wae xh xog", "nplurals=2; plural=(n != 1);" },
|
||||
{ "da", "nplurals=2; plural=(n != 1);" },
|
||||
{ "is", "nplurals=2; plural=(n%10==1 && n%100!=11);" },
|
||||
{ "mk", "nplurals=2; plural=(n%10==1 && n%100!=11);" },
|
||||
{ "ceb fil tl", "nplurals=2; plural=(n==1 || n==2 || n==3 || (n%10!=4 && n%10!=6 && n%10!=9));" },
|
||||
{ "lv prg", "nplurals=3; plural=(n%10==0 || (n%100>=11 && n%100<=19) ? 0 : n%10==1 && n%100!=11 ? 1 : 2);" },
|
||||
{ "lag", "nplurals=3; plural=(n==0 ? 0 : (n==0 || n==1) && n!=0 ? 1 : 2);" },
|
||||
{ "blo", "nplurals=3; plural=(n==0 ? 0 : n==1 ? 1 : 2);" },
|
||||
{ "ksh", "nplurals=3; plural=(n==0 ? 0 : n==1 ? 1 : 2);" },
|
||||
{ "he iw", "nplurals=3; plural=(n==1 ? 0 : n==2 ? 1 : 2);" },
|
||||
{ "iu naq sat se sma smi smj smn sms", "nplurals=3; plural=(n==1 ? 0 : n==2 ? 1 : 2);" },
|
||||
{ "shi", "nplurals=3; plural=(n==0 || n==1 ? 0 : n>=2 && n<=10 ? 1 : 2);" },
|
||||
{ "mo ro", "nplurals=3; plural=(n==1 ? 0 : n==0 || (n!=1 && n%100>=1 && n%100<=19) ? 1 : 2);" },
|
||||
{ "bs hr sh sr", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);" },
|
||||
{ "fr", "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);" },
|
||||
{ "pt", "nplurals=3; plural=((n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);" },
|
||||
{ "ca it lld pt_PT scn vec", "nplurals=3; plural=(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);" },
|
||||
{ "es", "nplurals=3; plural=(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);" },
|
||||
{ "gd", "nplurals=4; plural=(n==1 || n==11 ? 0 : n==2 || n==12 ? 1 : (n>=3 && n<=10) || (n>=13 && n<=19) ? 2 : 3);" },
|
||||
{ "sl", "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n%100<=4 ? 2 : 3);" },
|
||||
{ "dsb hsb", "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n%100<=4 ? 2 : 3);" },
|
||||
{ "cs sk", "nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 ? 1 : 2);" },
|
||||
{ "pl", "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);" },
|
||||
{ "be", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);" },
|
||||
{ "lt", "nplurals=3; plural=(n%10==1 && (n%100<11 || n%100>19) ? 0 : n%10>=2 && n%10<=9 && (n%100<11 || n%100>19) ? 1 : 2);" },
|
||||
{ "ru uk", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);" },
|
||||
{ "br", "nplurals=5; plural=(n%10==1 && n%100!=11 && n%100!=71 && n%100!=91 ? 0 : n%10==2 && n%100!=12 && n%100!=72 && n%100!=92 ? 1 : ((n%10>=3 && n%10<=4) || n%10==9) && (n%100<10 || n%100>19) && (n%100<70 || n%100>79) && (n%100<90 || n%100>99) ? 2 : n!=0 && n%1000000==0 ? 3 : 4);" },
|
||||
{ "mt", "nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n==0 || (n%100>=3 && n%100<=10) ? 2 : n%100>=11 && n%100<=19 ? 3 : 4);" },
|
||||
{ "ga", "nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4);" },
|
||||
{ "gv", "nplurals=4; plural=(n%10==1 ? 0 : n%10==2 ? 1 : n%100==0 || n%100==20 || n%100==40 || n%100==60 || n%100==80 ? 2 : 3);" },
|
||||
{ "kw", "nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n%100==2 || n%100==22 || n%100==42 || n%100==62 || n%100==82 || (n%1000==0 && ((n%100000>=1000 && n%100000<=20000) || n%100000==40000 || n%100000==60000 || n%100000==80000)) || (n!=0 && n%1000000==100000) ? 2 : n%100==3 || n%100==23 || n%100==43 || n%100==63 || n%100==83 ? 3 : n!=1 && (n%100==1 || n%100==21 || n%100==41 || n%100==61 || n%100==81) ? 4 : 5);" },
|
||||
{ "ar ars", "nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);" },
|
||||
{ "cy", "nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 5);" },
|
||||
{ nullptr, nullptr },
|
||||
};
|
||||
|
|
|
@ -44,11 +44,27 @@ struct CompressedString {
|
|||
|
||||
void OptimizedTranslation::generate(const Ref<Translation> &p_from) {
|
||||
// This method compresses a Translation instance.
|
||||
// Right now, it doesn't handle context or plurals, so Translation subclasses using plurals or context (i.e TranslationPO) shouldn't be compressed.
|
||||
// Right now, it doesn't handle context or plurals.
|
||||
#ifdef TOOLS_ENABLED
|
||||
ERR_FAIL_COND(p_from.is_null());
|
||||
|
||||
List<StringName> keys;
|
||||
p_from->get_message_list(&keys);
|
||||
{
|
||||
List<StringName> raw_keys;
|
||||
p_from->get_message_list(&raw_keys);
|
||||
|
||||
for (const StringName &key : raw_keys) {
|
||||
const String key_str = key.operator String();
|
||||
int p = key_str.find_char(0x04);
|
||||
if (p == -1) {
|
||||
keys.push_back(key);
|
||||
} else {
|
||||
const String &msgctxt = key_str.substr(0, p);
|
||||
const String &msgid = key_str.substr(p + 1);
|
||||
WARN_PRINT(vformat("OptimizedTranslation does not support context, ignoring message '%s' with context '%s'.", msgid, msgctxt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int size = Math::larger_prime(keys.size());
|
||||
|
||||
|
|
167
core/string/plural_rules.cpp
Normal file
167
core/string/plural_rules.cpp
Normal file
|
@ -0,0 +1,167 @@
|
|||
/**************************************************************************/
|
||||
/* plural_rules.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "plural_rules.h"
|
||||
|
||||
#include "core/math/expression.h"
|
||||
|
||||
int PluralRules::_eq_test(const Array &p_input_val, const Ref<EQNode> &p_node, const Variant &p_result) const {
|
||||
if (p_node.is_null()) {
|
||||
return p_result;
|
||||
}
|
||||
|
||||
static const Vector<String> input_name = { "n" };
|
||||
|
||||
Error err = expr->parse(p_node->regex, input_name);
|
||||
ERR_FAIL_COND_V_MSG(err != OK, 0, vformat("Cannot parse expression \"%s\". Error: %s", p_node->regex, expr->get_error_text()));
|
||||
|
||||
Variant result = expr->execute(p_input_val);
|
||||
ERR_FAIL_COND_V_MSG(expr->has_execute_failed(), 0, vformat("Cannot evaluate expression \"%s\".", p_node->regex));
|
||||
|
||||
if (bool(result)) {
|
||||
return _eq_test(p_input_val, p_node->left, result);
|
||||
} else {
|
||||
return _eq_test(p_input_val, p_node->right, result);
|
||||
}
|
||||
}
|
||||
|
||||
int PluralRules::_find_unquoted(const String &p_src, char32_t p_chr) const {
|
||||
const int len = p_src.length();
|
||||
if (len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char32_t *src = p_src.get_data();
|
||||
bool in_quote = false;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (in_quote) {
|
||||
if (src[i] == ')') {
|
||||
in_quote = false;
|
||||
}
|
||||
} else {
|
||||
if (src[i] == '(') {
|
||||
in_quote = true;
|
||||
} else if (src[i] == p_chr) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void PluralRules::_cache_plural_tests(const String &p_plural_rule, Ref<EQNode> &p_node) {
|
||||
// Some examples of p_plural_rule passed in can have the form:
|
||||
// "n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5" (Arabic)
|
||||
// "n >= 2" (French) // When evaluating the last, especially careful with this one.
|
||||
// "n != 1" (English)
|
||||
|
||||
String rule = p_plural_rule;
|
||||
if (rule.begins_with("(") && rule.ends_with(")")) {
|
||||
int bcount = 0;
|
||||
for (int i = 1; i < rule.length() - 1 && bcount >= 0; i++) {
|
||||
if (rule[i] == '(') {
|
||||
bcount++;
|
||||
} else if (rule[i] == ')') {
|
||||
bcount--;
|
||||
}
|
||||
}
|
||||
if (bcount == 0) {
|
||||
rule = rule.substr(1, rule.length() - 2);
|
||||
}
|
||||
}
|
||||
|
||||
int first_ques_mark = _find_unquoted(rule, '?');
|
||||
int first_colon = _find_unquoted(rule, ':');
|
||||
|
||||
if (first_ques_mark == -1) {
|
||||
p_node->regex = rule.strip_edges();
|
||||
return;
|
||||
}
|
||||
|
||||
p_node->regex = rule.substr(0, first_ques_mark).strip_edges();
|
||||
|
||||
p_node->left.instantiate();
|
||||
_cache_plural_tests(rule.substr(first_ques_mark + 1, first_colon - first_ques_mark - 1).strip_edges(), p_node->left);
|
||||
p_node->right.instantiate();
|
||||
_cache_plural_tests(rule.substr(first_colon + 1).strip_edges(), p_node->right);
|
||||
}
|
||||
|
||||
int PluralRules::evaluate(int p_n) const {
|
||||
const int *cached = cache.getptr(p_n);
|
||||
if (cached) {
|
||||
return *cached;
|
||||
}
|
||||
|
||||
const Array &input_val = { p_n };
|
||||
int index = _eq_test(input_val, equi_tests, 0);
|
||||
cache.insert(p_n, index);
|
||||
return index;
|
||||
}
|
||||
|
||||
PluralRules::PluralRules(int p_nplurals, const String &p_plural) :
|
||||
nplurals(p_nplurals),
|
||||
plural(p_plural) {
|
||||
equi_tests.instantiate();
|
||||
_cache_plural_tests(plural, equi_tests);
|
||||
|
||||
expr.instantiate();
|
||||
}
|
||||
|
||||
PluralRules *PluralRules::parse(const String &p_rules) {
|
||||
// `p_rules` should be in the format "nplurals=<N>; plural=<Expression>;".
|
||||
|
||||
const int nplurals_eq = p_rules.find_char('=');
|
||||
ERR_FAIL_COND_V_MSG(nplurals_eq == -1, nullptr, "Invalid plural rules format. Missing equal sign for `nplurals`.");
|
||||
|
||||
const int nplurals_semi_col = p_rules.find_char(';', nplurals_eq);
|
||||
ERR_FAIL_COND_V_MSG(nplurals_semi_col == -1, nullptr, "Invalid plural rules format. Missing semicolon for `nplurals`.");
|
||||
|
||||
const String nplurals_str = p_rules.substr(nplurals_eq + 1, nplurals_semi_col - (nplurals_eq + 1)).strip_edges();
|
||||
ERR_FAIL_COND_V_MSG(!nplurals_str.is_valid_int(), nullptr, "Invalid plural rules format. `nplurals` should be an integer.");
|
||||
|
||||
const int nplurals = nplurals_str.to_int();
|
||||
ERR_FAIL_COND_V_MSG(nplurals < 1, nullptr, "Invalid plural rules format. `nplurals` should be at least 1.");
|
||||
|
||||
const int expression_eq = p_rules.find_char('=', nplurals_semi_col + 1);
|
||||
ERR_FAIL_COND_V_MSG(expression_eq == -1, nullptr, "Invalid plural rules format. Missing equal sign for `plural`.");
|
||||
|
||||
int expression_end = p_rules.rfind_char(';');
|
||||
if (expression_end == -1) {
|
||||
WARN_PRINT("Invalid plural rules format. Missing semicolon at the end of `plural` expression. Assuming ends at the end of the string.");
|
||||
expression_end = p_rules.length();
|
||||
}
|
||||
|
||||
const int expression_start = expression_eq + 1;
|
||||
ERR_FAIL_COND_V_MSG(expression_end <= expression_start, nullptr, "Invalid plural rules format. `plural` expression is empty.");
|
||||
|
||||
const String &plural = p_rules.substr(expression_start, expression_end - expression_start).strip_edges();
|
||||
return memnew(PluralRules(nplurals, plural));
|
||||
}
|
72
core/string/plural_rules.h
Normal file
72
core/string/plural_rules.h
Normal file
|
@ -0,0 +1,72 @@
|
|||
/**************************************************************************/
|
||||
/* plural_rules.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/object/ref_counted.h"
|
||||
#include "core/templates/lru.h"
|
||||
|
||||
class Expression;
|
||||
|
||||
class PluralRules : public Object {
|
||||
GDSOFTCLASS(PluralRules, Object);
|
||||
|
||||
mutable LRUCache<int, int> cache;
|
||||
|
||||
// These two fields are initialized in the constructor.
|
||||
const int nplurals;
|
||||
const String plural;
|
||||
|
||||
// Cache temporary variables related to `evaluate()` to make it faster.
|
||||
class EQNode : public RefCounted {
|
||||
GDSOFTCLASS(EQNode, RefCounted);
|
||||
|
||||
public:
|
||||
String regex;
|
||||
Ref<EQNode> left;
|
||||
Ref<EQNode> right;
|
||||
};
|
||||
Ref<EQNode> equi_tests;
|
||||
Ref<Expression> expr;
|
||||
|
||||
int _find_unquoted(const String &p_src, char32_t p_chr) const;
|
||||
int _eq_test(const Array &p_input_val, const Ref<EQNode> &p_node, const Variant &p_result) const;
|
||||
void _cache_plural_tests(const String &p_plural_rule, Ref<EQNode> &p_node);
|
||||
|
||||
PluralRules(int p_nplurals, const String &p_plural);
|
||||
|
||||
public:
|
||||
int evaluate(int p_n) const;
|
||||
|
||||
int get_nplurals() const { return nplurals; }
|
||||
String get_plural() const { return plural; }
|
||||
|
||||
static PluralRules *parse(const String &p_rules);
|
||||
};
|
|
@ -31,58 +31,132 @@
|
|||
#include "translation.h"
|
||||
|
||||
#include "core/os/thread.h"
|
||||
#include "core/string/plural_rules.h"
|
||||
#include "core/string/translation_server.h"
|
||||
|
||||
void _check_for_incompatibility(const String &p_msgctxt, const String &p_msgid) {
|
||||
// Gettext PO and MO files use an empty untranslated string without context
|
||||
// to store metadata.
|
||||
if (p_msgctxt.is_empty() && p_msgid.is_empty()) {
|
||||
WARN_PRINT("Both context and the untranslated string are empty. This may cause issues with the translation system and external tools.");
|
||||
}
|
||||
|
||||
// The EOT character (0x04) is used as a separator between context and
|
||||
// untranslated string in the MO file format. This convention is also used
|
||||
// by `get_message_list()`.
|
||||
//
|
||||
// It's unusual to have this character in the context or untranslated
|
||||
// string. But it doesn't do any harm as long as you are aware of this when
|
||||
// using the relevant APIs and tools.
|
||||
if (p_msgctxt.contains_char(0x04)) {
|
||||
WARN_PRINT(vformat("Found EOT character (0x04) within context '%s'. This may cause issues with the translation system and external tools.", p_msgctxt));
|
||||
}
|
||||
if (p_msgid.contains_char(0x04)) {
|
||||
WARN_PRINT(vformat("Found EOT character (0x04) within untranslated string '%s'. This may cause issues with the translation system and external tools.", p_msgid));
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary Translation::_get_messages() const {
|
||||
Dictionary d;
|
||||
for (const KeyValue<StringName, StringName> &E : translation_map) {
|
||||
d[E.key] = E.value;
|
||||
for (const KeyValue<MessageKey, Vector<StringName>> &E : translation_map) {
|
||||
const Array &storage_key = { E.key.msgctxt, E.key.msgid };
|
||||
|
||||
Array storage_value;
|
||||
storage_value.resize(E.value.size());
|
||||
for (int i = 0; i < E.value.size(); i++) {
|
||||
storage_value[i] = E.value[i];
|
||||
}
|
||||
d[storage_key] = storage_value;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
Vector<String> Translation::_get_message_list() const {
|
||||
Vector<String> msgs;
|
||||
msgs.resize(translation_map.size());
|
||||
int idx = 0;
|
||||
for (const KeyValue<StringName, StringName> &E : translation_map) {
|
||||
msgs.set(idx, E.key);
|
||||
idx += 1;
|
||||
}
|
||||
void Translation::_set_messages(const Dictionary &p_messages) {
|
||||
translation_map.clear();
|
||||
|
||||
return msgs;
|
||||
for (const KeyValue<Variant, Variant> &kv : p_messages) {
|
||||
switch (kv.key.get_type()) {
|
||||
// Old version, no context or plural support.
|
||||
case Variant::STRING_NAME: {
|
||||
const MessageKey msg_key = { StringName(), kv.key };
|
||||
_check_for_incompatibility(msg_key.msgctxt, msg_key.msgid);
|
||||
translation_map[msg_key] = { kv.value };
|
||||
} break;
|
||||
|
||||
// Current version.
|
||||
case Variant::ARRAY: {
|
||||
const Array &storage_key = kv.key;
|
||||
const MessageKey msg_key = { storage_key[0], storage_key[1] };
|
||||
|
||||
const Array &storage_value = kv.value;
|
||||
ERR_CONTINUE_MSG(storage_value.is_empty(), vformat("No translated strings for untranslated string '%s' with context '%s'.", msg_key.msgid, msg_key.msgctxt));
|
||||
|
||||
Vector<StringName> msgstrs;
|
||||
msgstrs.resize(storage_value.size());
|
||||
for (int i = 0; i < storage_value.size(); i++) {
|
||||
msgstrs.write[i] = storage_value[i];
|
||||
}
|
||||
|
||||
_check_for_incompatibility(msg_key.msgctxt, msg_key.msgid);
|
||||
translation_map[msg_key] = msgstrs;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
WARN_PRINT(vformat("Invalid key type in messages dictionary: %s.", Variant::get_type_name(kv.key.get_type())));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector<String> Translation::_get_message_list() const {
|
||||
List<StringName> msgstrs;
|
||||
get_message_list(&msgstrs);
|
||||
|
||||
Vector<String> keys;
|
||||
keys.resize(msgstrs.size());
|
||||
int idx = 0;
|
||||
for (const StringName &msgstr : msgstrs) {
|
||||
keys.write[idx++] = msgstr;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
Vector<String> Translation::get_translated_message_list() const {
|
||||
Vector<String> msgs;
|
||||
msgs.resize(translation_map.size());
|
||||
int idx = 0;
|
||||
for (const KeyValue<StringName, StringName> &E : translation_map) {
|
||||
msgs.set(idx, E.value);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
return msgs;
|
||||
}
|
||||
|
||||
void Translation::_set_messages(const Dictionary &p_messages) {
|
||||
for (const KeyValue<Variant, Variant> &kv : p_messages) {
|
||||
translation_map[kv.key] = kv.value;
|
||||
Vector<String> msgstrs;
|
||||
for (const KeyValue<MessageKey, Vector<StringName>> &E : translation_map) {
|
||||
for (const StringName &msgstr : E.value) {
|
||||
msgstrs.push_back(msgstr);
|
||||
}
|
||||
}
|
||||
return msgstrs;
|
||||
}
|
||||
|
||||
void Translation::set_locale(const String &p_locale) {
|
||||
locale = TranslationServer::get_singleton()->standardize_locale(p_locale);
|
||||
|
||||
if (plural_rules_cache && plural_rules_override.is_empty()) {
|
||||
memdelete(plural_rules_cache);
|
||||
plural_rules_cache = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) {
|
||||
translation_map[p_src_text] = p_xlated_text;
|
||||
_check_for_incompatibility(p_context, p_src_text);
|
||||
translation_map[{ p_context, p_src_text }] = { p_xlated_text };
|
||||
}
|
||||
|
||||
void Translation::add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context) {
|
||||
WARN_PRINT("Translation class doesn't handle plural messages. Calling add_plural_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles plurals, such as TranslationPO class");
|
||||
ERR_FAIL_COND_MSG(p_plural_xlated_texts.is_empty(), "Parameter vector p_plural_xlated_texts passed in is empty.");
|
||||
translation_map[p_src_text] = p_plural_xlated_texts[0];
|
||||
|
||||
Vector<StringName> msgstrs;
|
||||
msgstrs.resize(p_plural_xlated_texts.size());
|
||||
for (int i = 0; i < p_plural_xlated_texts.size(); i++) {
|
||||
msgstrs.write[i] = p_plural_xlated_texts[i];
|
||||
}
|
||||
|
||||
_check_for_incompatibility(p_context, p_src_text);
|
||||
translation_map[{ p_context, p_src_text }] = msgstrs;
|
||||
}
|
||||
|
||||
StringName Translation::get_message(const StringName &p_src_text, const StringName &p_context) const {
|
||||
|
@ -91,16 +165,13 @@ StringName Translation::get_message(const StringName &p_src_text, const StringNa
|
|||
return ret;
|
||||
}
|
||||
|
||||
if (p_context != StringName()) {
|
||||
WARN_PRINT("Translation class doesn't handle context. Using context in get_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class");
|
||||
}
|
||||
|
||||
HashMap<StringName, StringName>::ConstIterator E = translation_map.find(p_src_text);
|
||||
if (!E) {
|
||||
const Vector<StringName> *msgstrs = translation_map.getptr({ p_context, p_src_text });
|
||||
if (msgstrs == nullptr) {
|
||||
return StringName();
|
||||
}
|
||||
|
||||
return E->value;
|
||||
DEV_ASSERT(!msgstrs->is_empty()); // Should be prevented when adding messages.
|
||||
return msgstrs->get(0);
|
||||
}
|
||||
|
||||
StringName Translation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const {
|
||||
|
@ -109,21 +180,30 @@ StringName Translation::get_plural_message(const StringName &p_src_text, const S
|
|||
return ret;
|
||||
}
|
||||
|
||||
WARN_PRINT("Translation class doesn't handle plural messages. Calling get_plural_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles plurals, such as TranslationPO class");
|
||||
return get_message(p_src_text);
|
||||
ERR_FAIL_COND_V_MSG(p_n < 0, StringName(), "N passed into translation to get a plural message should not be negative. For negative numbers, use singular translation please. Search \"gettext PO Plural Forms\" online for details on translating negative numbers.");
|
||||
|
||||
const Vector<StringName> *msgstrs = translation_map.getptr({ p_context, p_src_text });
|
||||
if (msgstrs == nullptr) {
|
||||
return StringName();
|
||||
}
|
||||
|
||||
const int index = _get_plural_rules()->evaluate(p_n);
|
||||
ERR_FAIL_INDEX_V_MSG(index, msgstrs->size(), StringName(), "Plural index returned or number of plural translations is not valid.");
|
||||
return msgstrs->get(index);
|
||||
}
|
||||
|
||||
void Translation::erase_message(const StringName &p_src_text, const StringName &p_context) {
|
||||
if (p_context != StringName()) {
|
||||
WARN_PRINT("Translation class doesn't handle context. Using context in erase_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class");
|
||||
}
|
||||
|
||||
translation_map.erase(p_src_text);
|
||||
translation_map.erase({ p_context, p_src_text });
|
||||
}
|
||||
|
||||
void Translation::get_message_list(List<StringName> *r_messages) const {
|
||||
for (const KeyValue<StringName, StringName> &E : translation_map) {
|
||||
r_messages->push_back(E.key);
|
||||
for (const KeyValue<MessageKey, Vector<StringName>> &E : translation_map) {
|
||||
if (E.key.msgctxt.is_empty()) {
|
||||
r_messages->push_back(E.key.msgid);
|
||||
} else {
|
||||
// Separated by the EOT character. Compatible with the MO file format.
|
||||
r_messages->push_back(vformat("%s\x04%s", E.key.msgctxt, E.key.msgid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,6 +211,48 @@ int Translation::get_message_count() const {
|
|||
return translation_map.size();
|
||||
}
|
||||
|
||||
PluralRules *Translation::_get_plural_rules() const {
|
||||
if (plural_rules_cache) {
|
||||
return plural_rules_cache;
|
||||
}
|
||||
|
||||
if (!plural_rules_override.is_empty()) {
|
||||
plural_rules_cache = PluralRules::parse(plural_rules_override);
|
||||
}
|
||||
|
||||
if (!plural_rules_cache) {
|
||||
// Locale's default plural rules.
|
||||
const String &default_rule = TranslationServer::get_singleton()->get_plural_rules(locale);
|
||||
if (!default_rule.is_empty()) {
|
||||
plural_rules_cache = PluralRules::parse(default_rule);
|
||||
}
|
||||
|
||||
// Use English plural rules as a fallback.
|
||||
if (!plural_rules_cache) {
|
||||
plural_rules_cache = PluralRules::parse("nplurals=2; plural=(n != 1);");
|
||||
}
|
||||
}
|
||||
|
||||
DEV_ASSERT(plural_rules_cache != nullptr);
|
||||
return plural_rules_cache;
|
||||
}
|
||||
|
||||
void Translation::set_plural_rules_override(const String &p_rules) {
|
||||
plural_rules_override = p_rules;
|
||||
if (plural_rules_cache) {
|
||||
memdelete(plural_rules_cache);
|
||||
plural_rules_cache = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
String Translation::get_plural_rules_override() const {
|
||||
return plural_rules_override;
|
||||
}
|
||||
|
||||
int Translation::get_nplurals() const {
|
||||
return _get_plural_rules()->get_nplurals();
|
||||
}
|
||||
|
||||
void Translation::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("set_locale", "locale"), &Translation::set_locale);
|
||||
ClassDB::bind_method(D_METHOD("get_locale"), &Translation::get_locale);
|
||||
|
@ -144,10 +266,20 @@ void Translation::_bind_methods() {
|
|||
ClassDB::bind_method(D_METHOD("get_message_count"), &Translation::get_message_count);
|
||||
ClassDB::bind_method(D_METHOD("_set_messages", "messages"), &Translation::_set_messages);
|
||||
ClassDB::bind_method(D_METHOD("_get_messages"), &Translation::_get_messages);
|
||||
ClassDB::bind_method(D_METHOD("set_plural_rules_override", "rules"), &Translation::set_plural_rules_override);
|
||||
ClassDB::bind_method(D_METHOD("get_plural_rules_override"), &Translation::get_plural_rules_override);
|
||||
|
||||
GDVIRTUAL_BIND(_get_plural_message, "src_message", "src_plural_message", "n", "context");
|
||||
GDVIRTUAL_BIND(_get_message, "src_message", "context");
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "messages", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_messages", "_get_messages");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale"), "set_locale", "get_locale");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale", PROPERTY_HINT_LOCALE_ID), "set_locale", "get_locale");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::STRING, "plural_rules_override"), "set_plural_rules_override", "get_plural_rules_override");
|
||||
}
|
||||
|
||||
Translation::~Translation() {
|
||||
if (plural_rules_cache) {
|
||||
memdelete(plural_rules_cache);
|
||||
plural_rules_cache = nullptr;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,21 +33,45 @@
|
|||
#include "core/io/resource.h"
|
||||
#include "core/object/gdvirtual.gen.inc"
|
||||
|
||||
class PluralRules;
|
||||
|
||||
class Translation : public Resource {
|
||||
GDCLASS(Translation, Resource);
|
||||
OBJ_SAVE_TYPE(Translation);
|
||||
RES_BASE_EXTENSION("translation");
|
||||
|
||||
String locale = "en";
|
||||
HashMap<StringName, StringName> translation_map;
|
||||
|
||||
struct MessageKey {
|
||||
StringName msgctxt;
|
||||
StringName msgid;
|
||||
|
||||
// Required to use this struct as a key in HashMap.
|
||||
static uint32_t hash(const MessageKey &p_key) {
|
||||
uint32_t h = hash_murmur3_one_32(HashMapHasherDefault::hash(p_key.msgctxt));
|
||||
return hash_fmix32(hash_murmur3_one_32(HashMapHasherDefault::hash(p_key.msgid), h));
|
||||
}
|
||||
bool operator==(const MessageKey &p_key) const {
|
||||
return msgctxt == p_key.msgctxt && msgid == p_key.msgid;
|
||||
}
|
||||
};
|
||||
|
||||
HashMap<MessageKey, Vector<StringName>, MessageKey> translation_map;
|
||||
|
||||
mutable PluralRules *plural_rules_cache = nullptr;
|
||||
String plural_rules_override;
|
||||
|
||||
virtual Vector<String> _get_message_list() const;
|
||||
|
||||
// For data storage.
|
||||
virtual Dictionary _get_messages() const;
|
||||
virtual void _set_messages(const Dictionary &p_messages);
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
PluralRules *_get_plural_rules() const;
|
||||
|
||||
GDVIRTUAL2RC(StringName, _get_message, StringName, StringName);
|
||||
GDVIRTUAL4RC(StringName, _get_plural_message, StringName, StringName, int, StringName);
|
||||
|
||||
|
@ -64,5 +88,11 @@ public:
|
|||
virtual int get_message_count() const;
|
||||
virtual Vector<String> get_translated_message_list() const;
|
||||
|
||||
Translation() {}
|
||||
void set_plural_rules_override(const String &p_rules);
|
||||
String get_plural_rules_override() const;
|
||||
|
||||
// This method is not exposed to scripting intentionally. It is only used by TranslationLoaderPO and tests.
|
||||
int get_nplurals() const;
|
||||
|
||||
~Translation();
|
||||
};
|
||||
|
|
|
@ -202,7 +202,6 @@ StringName TranslationDomain::get_message_from_translations(const String &p_loca
|
|||
int best_score = 0;
|
||||
|
||||
for (const Ref<Translation> &E : translations) {
|
||||
ERR_CONTINUE(E.is_null());
|
||||
int score = TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale());
|
||||
if (score > 0 && score >= best_score) {
|
||||
const StringName r = E->get_message(p_message, p_context);
|
||||
|
@ -225,7 +224,6 @@ StringName TranslationDomain::get_message_from_translations(const String &p_loca
|
|||
int best_score = 0;
|
||||
|
||||
for (const Ref<Translation> &E : translations) {
|
||||
ERR_CONTINUE(E.is_null());
|
||||
int score = TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale());
|
||||
if (score > 0 && score >= best_score) {
|
||||
const StringName r = E->get_plural_message(p_message, p_message_plural, p_n, p_context);
|
||||
|
@ -246,7 +244,6 @@ StringName TranslationDomain::get_message_from_translations(const String &p_loca
|
|||
PackedStringArray TranslationDomain::get_loaded_locales() const {
|
||||
PackedStringArray locales;
|
||||
for (const Ref<Translation> &E : translations) {
|
||||
ERR_CONTINUE(E.is_null());
|
||||
const String &locale = E->get_locale();
|
||||
if (!locales.has(locale)) {
|
||||
locales.push_back(locale);
|
||||
|
@ -255,13 +252,20 @@ PackedStringArray TranslationDomain::get_loaded_locales() const {
|
|||
return locales;
|
||||
}
|
||||
|
||||
bool TranslationDomain::has_translation_for_locale(const String &p_locale) const {
|
||||
for (const Ref<Translation> &E : translations) {
|
||||
if (E->get_locale() == p_locale) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Translation objects that could potentially be used for the given locale.
|
||||
HashSet<Ref<Translation>> TranslationDomain::get_potential_translations(const String &p_locale) const {
|
||||
HashSet<Ref<Translation>> res;
|
||||
|
||||
for (const Ref<Translation> &E : translations) {
|
||||
ERR_CONTINUE(E.is_null());
|
||||
|
||||
if (TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale()) > 0) {
|
||||
res.insert(E);
|
||||
}
|
||||
|
@ -274,8 +278,6 @@ Ref<Translation> TranslationDomain::get_translation_object(const String &p_local
|
|||
int best_score = 0;
|
||||
|
||||
for (const Ref<Translation> &E : translations) {
|
||||
ERR_CONTINUE(E.is_null());
|
||||
|
||||
int score = TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale());
|
||||
if (score > 0 && score >= best_score) {
|
||||
res = E;
|
||||
|
@ -289,6 +291,7 @@ Ref<Translation> TranslationDomain::get_translation_object(const String &p_local
|
|||
}
|
||||
|
||||
void TranslationDomain::add_translation(const Ref<Translation> &p_translation) {
|
||||
ERR_FAIL_COND_MSG(p_translation.is_null(), "Invalid translation provided.");
|
||||
translations.insert(p_translation);
|
||||
}
|
||||
|
||||
|
|
|
@ -71,6 +71,7 @@ public:
|
|||
StringName get_message_from_translations(const String &p_locale, const StringName &p_message, const StringName &p_context) const;
|
||||
StringName get_message_from_translations(const String &p_locale, const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const;
|
||||
PackedStringArray get_loaded_locales() const;
|
||||
bool has_translation_for_locale(const String &p_locale) const;
|
||||
HashSet<Ref<Translation>> get_potential_translations(const String &p_locale) const;
|
||||
|
||||
public:
|
||||
|
|
|
@ -30,316 +30,5 @@
|
|||
|
||||
#include "translation_po.h"
|
||||
|
||||
#ifdef DEBUG_TRANSLATION_PO
|
||||
#include "core/io/file_access.h"
|
||||
|
||||
void TranslationPO::print_translation_map() {
|
||||
Error err;
|
||||
Ref<FileAccess> file = FileAccess::open("translation_map_print_test.txt", FileAccess::WRITE, &err);
|
||||
if (err != OK) {
|
||||
ERR_PRINT("Failed to open translation_map_print_test.txt");
|
||||
return;
|
||||
}
|
||||
|
||||
file->store_line("NPlural : " + String::num_int64(get_plural_forms()));
|
||||
file->store_line("Plural rule : " + get_plural_rule());
|
||||
file->store_line("");
|
||||
|
||||
List<StringName> context_l;
|
||||
translation_map.get_key_list(&context_l);
|
||||
for (const StringName &ctx : context_l) {
|
||||
file->store_line(" ===== Context: " + String::utf8(String(ctx).utf8()) + " ===== ");
|
||||
const HashMap<StringName, Vector<StringName>> &inner_map = translation_map[ctx];
|
||||
|
||||
List<StringName> id_l;
|
||||
inner_map.get_key_list(&id_l);
|
||||
for (const StringName &id : id_l) {
|
||||
file->store_line("msgid: " + String::utf8(String(id).utf8()));
|
||||
for (int i = 0; i < inner_map[id].size(); i++) {
|
||||
file->store_line("msgstr[" + String::num_int64(i) + "]: " + String::utf8(String(inner_map[id][i]).utf8()));
|
||||
}
|
||||
file->store_line("");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Dictionary TranslationPO::_get_messages() const {
|
||||
// Return translation_map as a Dictionary.
|
||||
|
||||
Dictionary d;
|
||||
|
||||
for (const KeyValue<StringName, HashMap<StringName, Vector<StringName>>> &E : translation_map) {
|
||||
Dictionary d2;
|
||||
|
||||
for (const KeyValue<StringName, Vector<StringName>> &E2 : E.value) {
|
||||
d2[E2.key] = E2.value;
|
||||
}
|
||||
|
||||
d[E.key] = d2;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
void TranslationPO::_set_messages(const Dictionary &p_messages) {
|
||||
// Construct translation_map from a Dictionary.
|
||||
|
||||
for (const KeyValue<Variant, Variant> &kv : p_messages) {
|
||||
const Dictionary &id_str_map = kv.value;
|
||||
|
||||
HashMap<StringName, Vector<StringName>> temp_map;
|
||||
for (const KeyValue<Variant, Variant> &kv_id : id_str_map) {
|
||||
StringName id = kv_id.key;
|
||||
temp_map[id] = kv_id.value;
|
||||
}
|
||||
|
||||
translation_map[kv.key] = temp_map;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<String> TranslationPO::get_translated_message_list() const {
|
||||
Vector<String> msgs;
|
||||
for (const KeyValue<StringName, HashMap<StringName, Vector<StringName>>> &E : translation_map) {
|
||||
if (E.key != StringName()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const KeyValue<StringName, Vector<StringName>> &E2 : E.value) {
|
||||
for (const StringName &E3 : E2.value) {
|
||||
msgs.push_back(E3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return msgs;
|
||||
}
|
||||
|
||||
Vector<String> TranslationPO::_get_message_list() const {
|
||||
// Return all keys in translation_map.
|
||||
|
||||
List<StringName> msgs;
|
||||
get_message_list(&msgs);
|
||||
|
||||
Vector<String> v;
|
||||
for (const StringName &E : msgs) {
|
||||
v.push_back(E);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
int TranslationPO::_get_plural_index(int p_n) const {
|
||||
// Get a number between [0;number of plural forms).
|
||||
|
||||
input_val.clear();
|
||||
input_val.push_back(p_n);
|
||||
|
||||
return _eq_test(equi_tests, 0);
|
||||
}
|
||||
|
||||
int TranslationPO::_eq_test(const Ref<EQNode> &p_node, const Variant &p_result) const {
|
||||
if (p_node.is_valid()) {
|
||||
Error err = expr->parse(p_node->regex, input_name);
|
||||
ERR_FAIL_COND_V_MSG(err != OK, 0, vformat("Cannot parse expression \"%s\". Error: %s", p_node->regex, expr->get_error_text()));
|
||||
|
||||
Variant result = expr->execute(input_val);
|
||||
ERR_FAIL_COND_V_MSG(expr->has_execute_failed(), 0, vformat("Cannot evaluate expression \"%s\".", p_node->regex));
|
||||
|
||||
if (bool(result)) {
|
||||
return _eq_test(p_node->left, result);
|
||||
} else {
|
||||
return _eq_test(p_node->right, result);
|
||||
}
|
||||
} else {
|
||||
return p_result;
|
||||
}
|
||||
}
|
||||
|
||||
int TranslationPO::_find_unquoted(const String &p_src, char32_t p_chr) const {
|
||||
const int len = p_src.length();
|
||||
if (len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char32_t *src = p_src.get_data();
|
||||
bool in_quote = false;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (in_quote) {
|
||||
if (src[i] == ')') {
|
||||
in_quote = false;
|
||||
}
|
||||
} else {
|
||||
if (src[i] == '(') {
|
||||
in_quote = true;
|
||||
} else if (src[i] == p_chr) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TranslationPO::_cache_plural_tests(const String &p_plural_rule, Ref<EQNode> &p_node) {
|
||||
// Some examples of p_plural_rule passed in can have the form:
|
||||
// "n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5" (Arabic)
|
||||
// "n >= 2" (French) // When evaluating the last, especially careful with this one.
|
||||
// "n != 1" (English)
|
||||
|
||||
String rule = p_plural_rule;
|
||||
if (rule.begins_with("(") && rule.ends_with(")")) {
|
||||
int bcount = 0;
|
||||
for (int i = 1; i < rule.length() - 1 && bcount >= 0; i++) {
|
||||
if (rule[i] == '(') {
|
||||
bcount++;
|
||||
} else if (rule[i] == ')') {
|
||||
bcount--;
|
||||
}
|
||||
}
|
||||
if (bcount == 0) {
|
||||
rule = rule.substr(1, rule.length() - 2);
|
||||
}
|
||||
}
|
||||
|
||||
int first_ques_mark = _find_unquoted(rule, '?');
|
||||
int first_colon = _find_unquoted(rule, ':');
|
||||
|
||||
if (first_ques_mark == -1) {
|
||||
p_node->regex = rule.strip_edges();
|
||||
return;
|
||||
}
|
||||
|
||||
p_node->regex = rule.substr(0, first_ques_mark).strip_edges();
|
||||
|
||||
p_node->left.instantiate();
|
||||
_cache_plural_tests(rule.substr(first_ques_mark + 1, first_colon - first_ques_mark - 1).strip_edges(), p_node->left);
|
||||
p_node->right.instantiate();
|
||||
_cache_plural_tests(rule.substr(first_colon + 1).strip_edges(), p_node->right);
|
||||
}
|
||||
|
||||
void TranslationPO::set_plural_rule(const String &p_plural_rule) {
|
||||
// Set plural_forms and plural_rule.
|
||||
// p_plural_rule passed in has the form "Plural-Forms: nplurals=2; plural=(n >= 2);".
|
||||
|
||||
int first_semi_col = p_plural_rule.find_char(';');
|
||||
plural_forms = p_plural_rule.substr(p_plural_rule.find_char('=') + 1, first_semi_col - (p_plural_rule.find_char('=') + 1)).to_int();
|
||||
|
||||
int expression_start = p_plural_rule.find_char('=', first_semi_col) + 1;
|
||||
int second_semi_col = p_plural_rule.rfind_char(';');
|
||||
plural_rule = p_plural_rule.substr(expression_start, second_semi_col - expression_start).strip_edges();
|
||||
|
||||
// Setup the cache to make evaluating plural rule faster later on.
|
||||
equi_tests.instantiate();
|
||||
_cache_plural_tests(plural_rule, equi_tests);
|
||||
|
||||
expr.instantiate();
|
||||
input_name.push_back("n");
|
||||
}
|
||||
|
||||
void TranslationPO::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) {
|
||||
HashMap<StringName, Vector<StringName>> &map_id_str = translation_map[p_context];
|
||||
|
||||
if (map_id_str.has(p_src_text)) {
|
||||
WARN_PRINT(vformat("Double translations for \"%s\" under the same context \"%s\" for locale \"%s\".\nThere should only be one unique translation for a given string under the same context.", String(p_src_text), String(p_context), get_locale()));
|
||||
map_id_str[p_src_text].set(0, p_xlated_text);
|
||||
} else {
|
||||
map_id_str[p_src_text].push_back(p_xlated_text);
|
||||
}
|
||||
}
|
||||
|
||||
void TranslationPO::add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context) {
|
||||
ERR_FAIL_COND_MSG(p_plural_xlated_texts.size() != plural_forms, vformat("Trying to add plural texts that don't match the required number of plural forms for locale \"%s\".", get_locale()));
|
||||
|
||||
HashMap<StringName, Vector<StringName>> &map_id_str = translation_map[p_context];
|
||||
|
||||
if (map_id_str.has(p_src_text)) {
|
||||
WARN_PRINT(vformat("Double translations for \"%s\" under the same context \"%s\" for locale %s.\nThere should only be one unique translation for a given string under the same context.", p_src_text, p_context, get_locale()));
|
||||
map_id_str[p_src_text].clear();
|
||||
}
|
||||
|
||||
for (int i = 0; i < p_plural_xlated_texts.size(); i++) {
|
||||
map_id_str[p_src_text].push_back(p_plural_xlated_texts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
int TranslationPO::get_plural_forms() const {
|
||||
return plural_forms;
|
||||
}
|
||||
|
||||
String TranslationPO::get_plural_rule() const {
|
||||
return plural_rule;
|
||||
}
|
||||
|
||||
StringName TranslationPO::get_message(const StringName &p_src_text, const StringName &p_context) const {
|
||||
if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) {
|
||||
return StringName();
|
||||
}
|
||||
ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].is_empty(), StringName(), vformat("Source text \"%s\" is registered but doesn't have a translation. Please report this bug.", String(p_src_text)));
|
||||
|
||||
return translation_map[p_context][p_src_text][0];
|
||||
}
|
||||
|
||||
StringName TranslationPO::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const {
|
||||
ERR_FAIL_COND_V_MSG(p_n < 0, StringName(), "N passed into translation to get a plural message should not be negative. For negative numbers, use singular translation please. Search \"gettext PO Plural Forms\" online for the documentation on translating negative numbers.");
|
||||
|
||||
// If the query is the same as last time, return the cached result.
|
||||
if (p_n == last_plural_n && p_context == last_plural_context && p_src_text == last_plural_key) {
|
||||
return translation_map[p_context][p_src_text][last_plural_mapped_index];
|
||||
}
|
||||
|
||||
if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) {
|
||||
return StringName();
|
||||
}
|
||||
ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].is_empty(), StringName(), vformat("Source text \"%s\" is registered but doesn't have a translation. Please report this bug.", String(p_src_text)));
|
||||
|
||||
int plural_index = _get_plural_index(p_n);
|
||||
ERR_FAIL_COND_V_MSG(plural_index < 0 || translation_map[p_context][p_src_text].size() < plural_index + 1, StringName(), "Plural index returned or number of plural translations is not valid. Please report this bug.");
|
||||
|
||||
// Cache result so that if the next entry is the same, we can return directly.
|
||||
// _get_plural_index(p_n) can get very costly, especially when evaluating long plural-rule (Arabic)
|
||||
last_plural_key = p_src_text;
|
||||
last_plural_context = p_context;
|
||||
last_plural_n = p_n;
|
||||
last_plural_mapped_index = plural_index;
|
||||
|
||||
return translation_map[p_context][p_src_text][plural_index];
|
||||
}
|
||||
|
||||
void TranslationPO::erase_message(const StringName &p_src_text, const StringName &p_context) {
|
||||
if (!translation_map.has(p_context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
translation_map[p_context].erase(p_src_text);
|
||||
}
|
||||
|
||||
void TranslationPO::get_message_list(List<StringName> *r_messages) const {
|
||||
// OptimizedTranslation uses this function to get the list of msgid.
|
||||
// Return all the keys of translation_map under "" context.
|
||||
|
||||
for (const KeyValue<StringName, HashMap<StringName, Vector<StringName>>> &E : translation_map) {
|
||||
if (E.key != StringName()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const KeyValue<StringName, Vector<StringName>> &E2 : E.value) {
|
||||
r_messages->push_back(E2.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int TranslationPO::get_message_count() const {
|
||||
int count = 0;
|
||||
|
||||
for (const KeyValue<StringName, HashMap<StringName, Vector<StringName>>> &E : translation_map) {
|
||||
count += E.value.size();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
void TranslationPO::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("get_plural_forms"), &TranslationPO::get_plural_forms);
|
||||
ClassDB::bind_method(D_METHOD("get_plural_rule"), &TranslationPO::get_plural_rule);
|
||||
}
|
||||
// This file is intentionally left empty.
|
||||
// It makes sure that `TranslationPO` exists, for compatibility.
|
||||
|
|
|
@ -30,73 +30,8 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
//#define DEBUG_TRANSLATION_PO
|
||||
|
||||
#include "core/math/expression.h"
|
||||
#include "core/string/translation.h"
|
||||
|
||||
class TranslationPO : public Translation {
|
||||
GDCLASS(TranslationPO, Translation);
|
||||
|
||||
// TLDR: Maps context to a list of source strings and translated strings. In PO terms, maps msgctxt to a list of msgid and msgstr.
|
||||
// The first key corresponds to context, and the second key (of the contained HashMap) corresponds to source string.
|
||||
// The value Vector<StringName> in the second map stores the translated strings. Index 0, 1, 2 matches msgstr[0], msgstr[1], msgstr[2]... in the case of plurals.
|
||||
// Otherwise index 0 matches to msgstr in a singular translation.
|
||||
// Strings without context have "" as first key.
|
||||
HashMap<StringName, HashMap<StringName, Vector<StringName>>> translation_map;
|
||||
|
||||
int plural_forms = 0; // 0 means no "Plural-Forms" is given in the PO header file. The min for all languages is 1.
|
||||
String plural_rule;
|
||||
|
||||
// Cache temporary variables related to _get_plural_index() to make it faster
|
||||
class EQNode : public RefCounted {
|
||||
GDSOFTCLASS(EQNode, RefCounted);
|
||||
|
||||
public:
|
||||
String regex;
|
||||
Ref<EQNode> left;
|
||||
Ref<EQNode> right;
|
||||
};
|
||||
Ref<EQNode> equi_tests;
|
||||
|
||||
int _find_unquoted(const String &p_src, char32_t p_chr) const;
|
||||
int _eq_test(const Ref<EQNode> &p_node, const Variant &p_result) const;
|
||||
|
||||
Vector<String> input_name;
|
||||
mutable Ref<Expression> expr;
|
||||
mutable Array input_val;
|
||||
mutable StringName last_plural_key;
|
||||
mutable StringName last_plural_context;
|
||||
mutable int last_plural_n = -1; // Set it to an impossible value at the beginning.
|
||||
mutable int last_plural_mapped_index = 0;
|
||||
|
||||
void _cache_plural_tests(const String &p_plural_rule, Ref<EQNode> &p_node);
|
||||
int _get_plural_index(int p_n) const;
|
||||
|
||||
Vector<String> _get_message_list() const override;
|
||||
Dictionary _get_messages() const override;
|
||||
void _set_messages(const Dictionary &p_messages) override;
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
public:
|
||||
Vector<String> get_translated_message_list() const override;
|
||||
void get_message_list(List<StringName> *r_messages) const override;
|
||||
int get_message_count() const override;
|
||||
void add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context = "") override;
|
||||
void add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context = "") override;
|
||||
StringName get_message(const StringName &p_src_text, const StringName &p_context = "") const override;
|
||||
StringName get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context = "") const override;
|
||||
void erase_message(const StringName &p_src_text, const StringName &p_context = "") override;
|
||||
|
||||
void set_plural_rule(const String &p_plural_rule);
|
||||
int get_plural_forms() const;
|
||||
String get_plural_rule() const;
|
||||
|
||||
#ifdef DEBUG_TRANSLATION_PO
|
||||
void print_translation_map();
|
||||
#endif
|
||||
|
||||
TranslationPO() {}
|
||||
};
|
||||
|
|
|
@ -37,15 +37,6 @@
|
|||
#include "core/os/os.h"
|
||||
#include "core/string/locales.h"
|
||||
|
||||
Vector<TranslationServer::LocaleScriptInfo> TranslationServer::locale_script_info;
|
||||
|
||||
HashMap<String, String> TranslationServer::language_map;
|
||||
HashMap<String, String> TranslationServer::script_map;
|
||||
HashMap<String, String> TranslationServer::locale_rename_map;
|
||||
HashMap<String, String> TranslationServer::country_name_map;
|
||||
HashMap<String, String> TranslationServer::variant_map;
|
||||
HashMap<String, String> TranslationServer::country_rename_map;
|
||||
|
||||
void TranslationServer::init_locale_info() {
|
||||
// Init locale info.
|
||||
language_map.clear();
|
||||
|
@ -114,6 +105,18 @@ void TranslationServer::init_locale_info() {
|
|||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Init plural rules.
|
||||
plural_rules_map.clear();
|
||||
idx = 0;
|
||||
while (plural_rules[idx][0] != nullptr) {
|
||||
const Vector<String> rule_locs = String(plural_rules[idx][0]).split(" ");
|
||||
const String rule = String(plural_rules[idx][1]);
|
||||
for (const String &l : rule_locs) {
|
||||
plural_rules_map[l] = rule;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
TranslationServer::Locale::operator String() const {
|
||||
|
@ -306,6 +309,26 @@ String TranslationServer::get_locale_name(const String &p_locale) const {
|
|||
return name;
|
||||
}
|
||||
|
||||
String TranslationServer::get_plural_rules(const String &p_locale) const {
|
||||
const String *rule = plural_rules_map.getptr(p_locale);
|
||||
if (rule) {
|
||||
return *rule;
|
||||
}
|
||||
|
||||
Locale l = Locale(*this, p_locale, false);
|
||||
if (!l.country.is_empty()) {
|
||||
rule = plural_rules_map.getptr(l.language + "_" + l.country);
|
||||
if (rule) {
|
||||
return *rule;
|
||||
}
|
||||
}
|
||||
rule = plural_rules_map.getptr(l.language);
|
||||
if (rule) {
|
||||
return *rule;
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
Vector<String> TranslationServer::get_all_languages() const {
|
||||
Vector<String> languages;
|
||||
|
||||
|
@ -471,8 +494,7 @@ void TranslationServer::setup() {
|
|||
String TranslationServer::get_tool_locale() {
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (Engine::get_singleton()->is_editor_hint() || Engine::get_singleton()->is_project_manager_hint()) {
|
||||
const PackedStringArray &locales = editor_domain->get_loaded_locales();
|
||||
if (locales.has(locale)) {
|
||||
if (editor_domain->has_translation_for_locale(locale)) {
|
||||
return locale;
|
||||
}
|
||||
return "en";
|
||||
|
@ -489,26 +511,6 @@ String TranslationServer::get_tool_locale() {
|
|||
}
|
||||
}
|
||||
|
||||
StringName TranslationServer::tool_translate(const StringName &p_message, const StringName &p_context) const {
|
||||
return editor_domain->translate(p_message, p_context);
|
||||
}
|
||||
|
||||
StringName TranslationServer::tool_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
|
||||
return editor_domain->translate_plural(p_message, p_message_plural, p_n, p_context);
|
||||
}
|
||||
|
||||
StringName TranslationServer::property_translate(const StringName &p_message, const StringName &p_context) const {
|
||||
return property_domain->translate(p_message, p_context);
|
||||
}
|
||||
|
||||
StringName TranslationServer::doc_translate(const StringName &p_message, const StringName &p_context) const {
|
||||
return doc_domain->translate(p_message, p_context);
|
||||
}
|
||||
|
||||
StringName TranslationServer::doc_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
|
||||
return doc_domain->translate_plural(p_message, p_message_plural, p_n, p_context);
|
||||
}
|
||||
|
||||
bool TranslationServer::is_pseudolocalization_enabled() const {
|
||||
return main_domain->is_pseudolocalization_enabled();
|
||||
}
|
||||
|
@ -585,6 +587,7 @@ void TranslationServer::_bind_methods() {
|
|||
ClassDB::bind_method(D_METHOD("get_country_name", "country"), &TranslationServer::get_country_name);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_locale_name", "locale"), &TranslationServer::get_locale_name);
|
||||
ClassDB::bind_method(D_METHOD("get_plural_rules", "locale"), &TranslationServer::get_plural_rules);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationServer::translate, DEFVAL(StringName()));
|
||||
ClassDB::bind_method(D_METHOD("translate_plural", "message", "plural_message", "n", "context"), &TranslationServer::translate_plural, DEFVAL(StringName()));
|
||||
|
@ -624,9 +627,14 @@ void TranslationServer::load_translations() {
|
|||
|
||||
TranslationServer::TranslationServer() {
|
||||
singleton = this;
|
||||
|
||||
main_domain.instantiate();
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
editor_domain = get_or_add_domain("godot.editor");
|
||||
property_domain = get_or_add_domain("godot.properties");
|
||||
doc_domain = get_or_add_domain("godot.documentation");
|
||||
#endif // TOOLS_ENABLED
|
||||
|
||||
init_locale_info();
|
||||
}
|
||||
|
|
|
@ -40,9 +40,11 @@ class TranslationServer : public Object {
|
|||
String fallback;
|
||||
|
||||
Ref<TranslationDomain> main_domain;
|
||||
#ifdef TOOLS_ENABLED
|
||||
Ref<TranslationDomain> editor_domain;
|
||||
Ref<TranslationDomain> property_domain;
|
||||
Ref<TranslationDomain> doc_domain;
|
||||
#endif // TOOLS_ENABLED
|
||||
HashMap<StringName, Ref<TranslationDomain>> custom_domains;
|
||||
|
||||
mutable HashMap<String, int> locale_compare_cache;
|
||||
|
@ -62,7 +64,7 @@ class TranslationServer : public Object {
|
|||
String default_country;
|
||||
HashSet<String> supported_countries;
|
||||
};
|
||||
static Vector<LocaleScriptInfo> locale_script_info;
|
||||
static inline Vector<LocaleScriptInfo> locale_script_info;
|
||||
|
||||
struct Locale {
|
||||
String language;
|
||||
|
@ -82,20 +84,26 @@ class TranslationServer : public Object {
|
|||
Locale(const TranslationServer &p_server, const String &p_locale, bool p_add_defaults);
|
||||
};
|
||||
|
||||
static HashMap<String, String> language_map;
|
||||
static HashMap<String, String> script_map;
|
||||
static HashMap<String, String> locale_rename_map;
|
||||
static HashMap<String, String> country_name_map;
|
||||
static HashMap<String, String> country_rename_map;
|
||||
static HashMap<String, String> variant_map;
|
||||
static inline HashMap<String, String> language_map;
|
||||
static inline HashMap<String, String> script_map;
|
||||
static inline HashMap<String, String> locale_rename_map;
|
||||
static inline HashMap<String, String> country_name_map;
|
||||
static inline HashMap<String, String> country_rename_map;
|
||||
static inline HashMap<String, String> variant_map;
|
||||
static inline HashMap<String, String> plural_rules_map;
|
||||
|
||||
void init_locale_info();
|
||||
|
||||
public:
|
||||
_FORCE_INLINE_ static TranslationServer *get_singleton() { return singleton; }
|
||||
|
||||
// Built-in domain accessors. For engine code only, user code should use `get_or_add_domain()` instead.
|
||||
Ref<TranslationDomain> get_main_domain() const { return main_domain; }
|
||||
#ifdef TOOLS_ENABLED
|
||||
Ref<TranslationDomain> get_editor_domain() const { return editor_domain; }
|
||||
Ref<TranslationDomain> get_property_domain() const { return property_domain; }
|
||||
Ref<TranslationDomain> get_doc_domain() const { return doc_domain; }
|
||||
#endif // TOOLS_ENABLED
|
||||
|
||||
void set_locale(const String &p_locale);
|
||||
String get_locale() const;
|
||||
|
@ -113,6 +121,7 @@ public:
|
|||
String get_country_name(const String &p_country) const;
|
||||
|
||||
String get_locale_name(const String &p_locale) const;
|
||||
String get_plural_rules(const String &p_locale) const;
|
||||
|
||||
PackedStringArray get_loaded_locales() const;
|
||||
|
||||
|
@ -133,11 +142,6 @@ public:
|
|||
int compare_locales(const String &p_locale_a, const String &p_locale_b) const;
|
||||
|
||||
String get_tool_locale();
|
||||
StringName tool_translate(const StringName &p_message, const StringName &p_context = "") const;
|
||||
StringName tool_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const;
|
||||
StringName property_translate(const StringName &p_message, const StringName &p_context = "") const;
|
||||
StringName doc_translate(const StringName &p_message, const StringName &p_context = "") const;
|
||||
StringName doc_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const;
|
||||
|
||||
bool has_domain(const StringName &p_domain) const;
|
||||
Ref<TranslationDomain> get_or_add_domain(const StringName &p_domain);
|
||||
|
|
|
@ -5854,7 +5854,7 @@ Vector<uint8_t> String::to_multibyte_char_buffer(const String &p_encoding) const
|
|||
*/
|
||||
String TTR(const String &p_text, const String &p_context) {
|
||||
if (TranslationServer::get_singleton()) {
|
||||
return TranslationServer::get_singleton()->tool_translate(p_text, p_context);
|
||||
return TranslationServer::get_singleton()->get_editor_domain()->translate(p_text, p_context);
|
||||
}
|
||||
|
||||
return p_text;
|
||||
|
@ -5874,7 +5874,7 @@ String TTR(const String &p_text, const String &p_context) {
|
|||
*/
|
||||
String TTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context) {
|
||||
if (TranslationServer::get_singleton()) {
|
||||
return TranslationServer::get_singleton()->tool_translate_plural(p_text, p_text_plural, p_n, p_context);
|
||||
return TranslationServer::get_singleton()->get_editor_domain()->translate_plural(p_text, p_text_plural, p_n, p_context);
|
||||
}
|
||||
|
||||
// Return message based on English plural rule if translation is not possible.
|
||||
|
@ -5895,7 +5895,7 @@ String DTR(const String &p_text, const String &p_context) {
|
|||
const String text = p_text.dedent().strip_edges();
|
||||
|
||||
if (TranslationServer::get_singleton()) {
|
||||
return String(TranslationServer::get_singleton()->doc_translate(text, p_context)).replace("$DOCS_URL", GODOT_VERSION_DOCS_URL);
|
||||
return String(TranslationServer::get_singleton()->get_doc_domain()->translate(text, p_context)).replace("$DOCS_URL", GODOT_VERSION_DOCS_URL);
|
||||
}
|
||||
|
||||
return text.replace("$DOCS_URL", GODOT_VERSION_DOCS_URL);
|
||||
|
@ -5912,7 +5912,7 @@ String DTRN(const String &p_text, const String &p_text_plural, int p_n, const St
|
|||
const String text_plural = p_text_plural.dedent().strip_edges();
|
||||
|
||||
if (TranslationServer::get_singleton()) {
|
||||
return String(TranslationServer::get_singleton()->doc_translate_plural(text, text_plural, p_n, p_context)).replace("$DOCS_URL", GODOT_VERSION_DOCS_URL);
|
||||
return String(TranslationServer::get_singleton()->get_doc_domain()->translate_plural(text, text_plural, p_n, p_context)).replace("$DOCS_URL", GODOT_VERSION_DOCS_URL);
|
||||
}
|
||||
|
||||
// Return message based on English plural rule if translation is not possible.
|
||||
|
@ -5936,11 +5936,13 @@ String DTRN(const String &p_text, const String &p_text_plural, int p_n, const St
|
|||
*/
|
||||
String RTR(const String &p_text, const String &p_context) {
|
||||
if (TranslationServer::get_singleton()) {
|
||||
String rtr = TranslationServer::get_singleton()->tool_translate(p_text, p_context);
|
||||
if (rtr.is_empty() || rtr == p_text) {
|
||||
return TranslationServer::get_singleton()->translate(p_text, p_context);
|
||||
#ifdef TOOLS_ENABLED
|
||||
String rtr = TranslationServer::get_singleton()->get_editor_domain()->translate(p_text, p_context);
|
||||
if (!rtr.is_empty() && rtr != p_text) {
|
||||
return rtr;
|
||||
}
|
||||
return rtr;
|
||||
#endif // TOOLS_ENABLED
|
||||
return TranslationServer::get_singleton()->translate(p_text, p_context);
|
||||
}
|
||||
|
||||
return p_text;
|
||||
|
@ -5959,11 +5961,13 @@ String RTR(const String &p_text, const String &p_context) {
|
|||
*/
|
||||
String RTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context) {
|
||||
if (TranslationServer::get_singleton()) {
|
||||
String rtr = TranslationServer::get_singleton()->tool_translate_plural(p_text, p_text_plural, p_n, p_context);
|
||||
if (rtr.is_empty() || rtr == p_text || rtr == p_text_plural) {
|
||||
return TranslationServer::get_singleton()->translate_plural(p_text, p_text_plural, p_n, p_context);
|
||||
#ifdef TOOLS_ENABLED
|
||||
String rtr = TranslationServer::get_singleton()->get_editor_domain()->translate_plural(p_text, p_text_plural, p_n, p_context);
|
||||
if (!rtr.is_empty() && rtr != p_text && rtr != p_text_plural) {
|
||||
return rtr;
|
||||
}
|
||||
return rtr;
|
||||
#endif // TOOLS_ENABLED
|
||||
return TranslationServer::get_singleton()->translate_plural(p_text, p_text_plural, p_n, p_context);
|
||||
}
|
||||
|
||||
// Return message based on English plural rule if translation is not possible.
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<description>
|
||||
Unlocks this [Mutex], leaving it to other threads.
|
||||
[b]Note:[/b] If a thread called [method lock] or [method try_lock] multiple times while already having ownership of the mutex, it must also call [method unlock] the same number of times in order to unlock it correctly.
|
||||
[b]Warning:[/b] Calling [method unlock] more times that [method lock] on a given thread, thus ending up trying to unlock a non-locked mutex, is wrong and may causes crashes or deadlocks.
|
||||
[b]Warning:[/b] Calling [method unlock] more times than [method lock] on a given thread, thus ending up trying to unlock a non-locked mutex, is wrong and may causes crashes or deadlocks.
|
||||
</description>
|
||||
</method>
|
||||
</methods>
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
A language translation that maps a collection of strings to their individual translations.
|
||||
</brief_description>
|
||||
<description>
|
||||
[Translation]s are resources that can be loaded and unloaded on demand. They map a collection of strings to their individual translations, and they also provide convenience methods for pluralization.
|
||||
[Translation] maps a collection of strings to their individual translations, and also provides convenience methods for pluralization.
|
||||
A [Translation] consists of messages. A message is identified by its context and untranslated string. Unlike [url=https://www.gnu.org/software/gettext/]gettext[/url], using an empty context string in Godot means not using any context.
|
||||
</description>
|
||||
<tutorials>
|
||||
<link title="Internationalizing games">$DOCS_URL/tutorials/i18n/internationalizing_games.html</link>
|
||||
|
@ -48,7 +49,6 @@
|
|||
<description>
|
||||
Adds a message involving plural translation if nonexistent, followed by its translation.
|
||||
An additional context could be used to specify the translation context or differentiate polysemic words.
|
||||
[b]Note:[/b] Plurals are only supported in [url=$DOCS_URL/tutorials/i18n/localization_using_gettext.html]gettext-based translations (PO)[/url], not CSV.
|
||||
</description>
|
||||
</method>
|
||||
<method name="erase_message">
|
||||
|
@ -76,7 +76,19 @@
|
|||
<method name="get_message_list" qualifiers="const">
|
||||
<return type="PackedStringArray" />
|
||||
<description>
|
||||
Returns all the messages (keys).
|
||||
Returns the keys of all messages, that is, the context and untranslated strings of each message.
|
||||
[b]Note:[/b] If a message does not use a context, the corresponding element is the untranslated string. Otherwise, the corresponding element is the context and untranslated string separated by the EOT character ([code]U+0004[/code]). This is done for compatibility purposes.
|
||||
[codeblock]
|
||||
for key in translation.get_message_list():
|
||||
var p = key.find("\u0004")
|
||||
if p == -1:
|
||||
var untranslated = key
|
||||
print("Message %s" % untranslated)
|
||||
else:
|
||||
var context = key.substr(0, p)
|
||||
var untranslated = key.substr(p + 1)
|
||||
print("Message %s with context %s" % [untranslated, context])
|
||||
[/codeblock]
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_plural_message" qualifiers="const">
|
||||
|
@ -94,7 +106,7 @@
|
|||
<method name="get_translated_message_list" qualifiers="const">
|
||||
<return type="PackedStringArray" />
|
||||
<description>
|
||||
Returns all the messages (translated text).
|
||||
Returns all the translated strings.
|
||||
</description>
|
||||
</method>
|
||||
</methods>
|
||||
|
@ -102,5 +114,9 @@
|
|||
<member name="locale" type="String" setter="set_locale" getter="get_locale" default=""en"">
|
||||
The locale of the translation.
|
||||
</member>
|
||||
<member name="plural_rules_override" type="String" setter="set_plural_rules_override" getter="get_plural_rules_override" default="""">
|
||||
The plural rules string to enforce. See [url=https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html]GNU gettext[/url] for examples and more info.
|
||||
If empty or invalid, default plural rules from [method TranslationServer.get_plural_rules] are used. The English plural rules are used as a fallback.
|
||||
</member>
|
||||
</members>
|
||||
</class>
|
||||
|
|
|
@ -92,6 +92,13 @@
|
|||
Returns the translation domain with the specified name. An empty translation domain will be created and added if it does not exist.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_plural_rules" qualifiers="const">
|
||||
<return type="String" />
|
||||
<param index="0" name="locale" type="String" />
|
||||
<description>
|
||||
Returns the default plural rules for the [param locale].
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_script_name" qualifiers="const">
|
||||
<return type="String" />
|
||||
<param index="0" name="script" type="String" />
|
||||
|
|
|
@ -1867,6 +1867,78 @@ void RenderingDeviceDriverD3D12::texture_get_copyable_layout(TextureID p_texture
|
|||
r_layout->layer_pitch = subresource_total_size / tex_info->desc.ArraySize();
|
||||
}
|
||||
|
||||
Vector<uint8_t> RenderingDeviceDriverD3D12::texture_get_data(TextureID p_texture, uint32_t p_layer) {
|
||||
const TextureInfo *tex = (const TextureInfo *)p_texture.id;
|
||||
|
||||
DataFormat tex_format = tex->format;
|
||||
uint32_t tex_width = tex->desc.Width;
|
||||
uint32_t tex_height = tex->desc.Height;
|
||||
uint32_t tex_depth = tex->desc.DepthOrArraySize;
|
||||
uint32_t tex_mipmaps = tex->mipmaps;
|
||||
|
||||
uint32_t width, height, depth;
|
||||
uint32_t tight_mip_size = get_image_format_required_size(tex_format, tex_width, tex_height, tex_depth, tex_mipmaps, &width, &height, &depth);
|
||||
|
||||
Vector<uint8_t> image_data;
|
||||
image_data.resize(tight_mip_size);
|
||||
|
||||
uint32_t blockw, blockh;
|
||||
get_compressed_image_format_block_dimensions(tex_format, blockw, blockh);
|
||||
uint32_t block_size = get_compressed_image_format_block_byte_size(tex_format);
|
||||
uint32_t pixel_size = get_image_format_pixel_size(tex_format);
|
||||
|
||||
{
|
||||
uint8_t *w = image_data.ptrw();
|
||||
|
||||
uint32_t mipmap_offset = 0;
|
||||
for (uint32_t mm_i = 0; mm_i < tex_mipmaps; mm_i++) {
|
||||
uint32_t image_total = get_image_format_required_size(tex_format, tex_width, tex_height, tex_depth, mm_i + 1, &width, &height, &depth);
|
||||
|
||||
uint8_t *write_ptr_mipmap = w + mipmap_offset;
|
||||
tight_mip_size = image_total - mipmap_offset;
|
||||
|
||||
RDD::TextureSubresource subres;
|
||||
subres.aspect = RDD::TEXTURE_ASPECT_COLOR;
|
||||
subres.layer = p_layer;
|
||||
subres.mipmap = mm_i;
|
||||
RDD::TextureCopyableLayout layout;
|
||||
texture_get_copyable_layout(p_texture, subres, &layout);
|
||||
|
||||
uint8_t *img_mem = texture_map(p_texture, subres);
|
||||
ERR_FAIL_NULL_V(img_mem, Vector<uint8_t>());
|
||||
|
||||
for (uint32_t z = 0; z < depth; z++) {
|
||||
uint8_t *write_ptr = write_ptr_mipmap + z * tight_mip_size / depth;
|
||||
const uint8_t *slice_read_ptr = img_mem + z * layout.depth_pitch;
|
||||
|
||||
if (block_size > 1) {
|
||||
// Compressed.
|
||||
uint32_t line_width = (block_size * (width / blockw));
|
||||
for (uint32_t y = 0; y < height / blockh; y++) {
|
||||
const uint8_t *rptr = slice_read_ptr + y * layout.row_pitch;
|
||||
uint8_t *wptr = write_ptr + y * line_width;
|
||||
|
||||
memcpy(wptr, rptr, line_width);
|
||||
}
|
||||
} else {
|
||||
// Uncompressed.
|
||||
for (uint32_t y = 0; y < height; y++) {
|
||||
const uint8_t *rptr = slice_read_ptr + y * layout.row_pitch;
|
||||
uint8_t *wptr = write_ptr + y * pixel_size * width;
|
||||
memcpy(wptr, rptr, (uint64_t)pixel_size * width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
texture_unmap(p_texture);
|
||||
|
||||
mipmap_offset = image_total;
|
||||
}
|
||||
}
|
||||
|
||||
return image_data;
|
||||
}
|
||||
|
||||
uint8_t *RenderingDeviceDriverD3D12::texture_map(TextureID p_texture, const TextureSubresource &p_subresource) {
|
||||
TextureInfo *tex_info = (TextureInfo *)p_texture.id;
|
||||
#ifdef DEBUG_ENABLED
|
||||
|
|
|
@ -384,6 +384,7 @@ public:
|
|||
virtual void texture_free(TextureID p_texture) override final;
|
||||
virtual uint64_t texture_get_allocation_size(TextureID p_texture) override final;
|
||||
virtual void texture_get_copyable_layout(TextureID p_texture, const TextureSubresource &p_subresource, TextureCopyableLayout *r_layout) override final;
|
||||
virtual Vector<uint8_t> texture_get_data(TextureID p_texture, uint32_t p_layer) override final;
|
||||
virtual uint8_t *texture_map(TextureID p_texture, const TextureSubresource &p_subresource) override final;
|
||||
virtual void texture_unmap(TextureID p_texture) override final;
|
||||
virtual BitField<TextureUsageBits> texture_get_usages_supported_by_format(DataFormat p_format, bool p_cpu_readable) override final;
|
||||
|
|
|
@ -113,8 +113,7 @@ public:
|
|||
|
||||
private:
|
||||
// Returns true if the texture is a valid linear format.
|
||||
Result<bool> is_valid_linear(TextureFormat const &p_format) const;
|
||||
void _get_sub_resource(TextureID p_texture, const TextureSubresource &p_subresource, TextureCopyableLayout *r_layout) const;
|
||||
bool is_valid_linear(TextureFormat const &p_format) const;
|
||||
|
||||
public:
|
||||
virtual TextureID texture_create(const TextureFormat &p_format, const TextureView &p_view) override final;
|
||||
|
@ -124,6 +123,7 @@ public:
|
|||
virtual void texture_free(TextureID p_texture) override final;
|
||||
virtual uint64_t texture_get_allocation_size(TextureID p_texture) override final;
|
||||
virtual void texture_get_copyable_layout(TextureID p_texture, const TextureSubresource &p_subresource, TextureCopyableLayout *r_layout) override final;
|
||||
virtual Vector<uint8_t> texture_get_data(TextureID p_texture, uint32_t p_layer) override final;
|
||||
virtual uint8_t *texture_map(TextureID p_texture, const TextureSubresource &p_subresource) override final;
|
||||
virtual void texture_unmap(TextureID p_texture) override final;
|
||||
virtual BitField<TextureUsageBits> texture_get_usages_supported_by_format(DataFormat p_format, bool p_cpu_readable) override final;
|
||||
|
|
|
@ -187,25 +187,14 @@ static const MTLTextureType TEXTURE_TYPE[RD::TEXTURE_TYPE_MAX] = {
|
|||
MTLTextureTypeCubeArray,
|
||||
};
|
||||
|
||||
RenderingDeviceDriverMetal::Result<bool> RenderingDeviceDriverMetal::is_valid_linear(TextureFormat const &p_format) const {
|
||||
if (!flags::any(p_format.usage_bits, TEXTURE_USAGE_CPU_READ_BIT)) {
|
||||
return false;
|
||||
}
|
||||
bool RenderingDeviceDriverMetal::is_valid_linear(TextureFormat const &p_format) const {
|
||||
MTLFormatType ft = pixel_formats->getFormatType(p_format.format);
|
||||
|
||||
PixelFormats &pf = *pixel_formats;
|
||||
MTLFormatType ft = pf.getFormatType(p_format.format);
|
||||
|
||||
// Requesting a linear format, which has further restrictions, similar to Vulkan
|
||||
// when specifying VK_IMAGE_TILING_LINEAR.
|
||||
|
||||
ERR_FAIL_COND_V_MSG(p_format.texture_type != TEXTURE_TYPE_2D, ERR_CANT_CREATE, "Linear (TEXTURE_USAGE_CPU_READ_BIT) textures must be 2D");
|
||||
ERR_FAIL_COND_V_MSG(ft != MTLFormatType::DepthStencil, ERR_CANT_CREATE, "Linear (TEXTURE_USAGE_CPU_READ_BIT) textures must not be a depth/stencil format");
|
||||
ERR_FAIL_COND_V_MSG(ft != MTLFormatType::Compressed, ERR_CANT_CREATE, "Linear (TEXTURE_USAGE_CPU_READ_BIT) textures must not be a compressed format");
|
||||
ERR_FAIL_COND_V_MSG(p_format.mipmaps != 1, ERR_CANT_CREATE, "Linear (TEXTURE_USAGE_CPU_READ_BIT) textures must have 1 mipmap level");
|
||||
ERR_FAIL_COND_V_MSG(p_format.array_layers != 1, ERR_CANT_CREATE, "Linear (TEXTURE_USAGE_CPU_READ_BIT) textures must have 1 array layer");
|
||||
ERR_FAIL_COND_V_MSG(p_format.samples != TEXTURE_SAMPLES_1, ERR_CANT_CREATE, "Linear (TEXTURE_USAGE_CPU_READ_BIT) textures must have 1 sample");
|
||||
|
||||
return true;
|
||||
return p_format.texture_type == TEXTURE_TYPE_2D // Linear textures must be 2D textures.
|
||||
&& ft != MTLFormatType::DepthStencil && ft != MTLFormatType::Compressed // Linear textures must not be depth/stencil or compressed formats.)
|
||||
&& p_format.mipmaps == 1 // Linear textures must have 1 mipmap level.
|
||||
&& p_format.array_layers == 1 // Linear textures must have 1 array layer.
|
||||
&& p_format.samples == TEXTURE_SAMPLES_1; // Linear textures must have 1 sample.
|
||||
}
|
||||
|
||||
RDD::TextureID RenderingDeviceDriverMetal::texture_create(const TextureFormat &p_format, const TextureView &p_view) {
|
||||
|
@ -292,6 +281,7 @@ RDD::TextureID RenderingDeviceDriverMetal::texture_create(const TextureFormat &p
|
|||
// Usage.
|
||||
|
||||
MTLResourceOptions options = 0;
|
||||
bool is_linear = false;
|
||||
#if defined(VISIONOS_ENABLED)
|
||||
const bool supports_memoryless = true;
|
||||
#else
|
||||
|
@ -304,6 +294,11 @@ RDD::TextureID RenderingDeviceDriverMetal::texture_create(const TextureFormat &p
|
|||
options = MTLResourceCPUCacheModeDefaultCache | MTLResourceHazardTrackingModeTracked;
|
||||
if (p_format.usage_bits & TEXTURE_USAGE_CPU_READ_BIT) {
|
||||
options |= MTLResourceStorageModeShared;
|
||||
// The user has indicated they want to read from the texture on the CPU,
|
||||
// so we'll see if we can use a linear format.
|
||||
// A linear format is a texture that is backed by a buffer,
|
||||
// which allows for CPU access to the texture data via a pointer.
|
||||
is_linear = is_valid_linear(p_format);
|
||||
} else {
|
||||
options |= MTLResourceStorageModePrivate;
|
||||
}
|
||||
|
@ -358,13 +353,6 @@ RDD::TextureID RenderingDeviceDriverMetal::texture_create(const TextureFormat &p
|
|||
|
||||
// Allocate memory.
|
||||
|
||||
bool is_linear;
|
||||
{
|
||||
Result<bool> is_linear_or_err = is_valid_linear(p_format);
|
||||
ERR_FAIL_COND_V(std::holds_alternative<Error>(is_linear_or_err), TextureID());
|
||||
is_linear = std::get<bool>(is_linear_or_err);
|
||||
}
|
||||
|
||||
id<MTLTexture> obj = nil;
|
||||
if (is_linear) {
|
||||
// Linear textures are restricted to 2D textures, a single mipmap level and a single array layer.
|
||||
|
@ -525,114 +513,107 @@ uint64_t RenderingDeviceDriverMetal::texture_get_allocation_size(TextureID p_tex
|
|||
return obj.allocatedSize;
|
||||
}
|
||||
|
||||
void RenderingDeviceDriverMetal::_get_sub_resource(TextureID p_texture, const TextureSubresource &p_subresource, TextureCopyableLayout *r_layout) const {
|
||||
id<MTLTexture> obj = rid::get(p_texture);
|
||||
|
||||
*r_layout = {};
|
||||
|
||||
PixelFormats &pf = *pixel_formats;
|
||||
|
||||
size_t row_alignment = get_texel_buffer_alignment_for_format(obj.pixelFormat);
|
||||
size_t offset = 0;
|
||||
size_t array_layers = obj.arrayLength;
|
||||
MTLSize size = MTLSizeMake(obj.width, obj.height, obj.depth);
|
||||
MTLPixelFormat pixel_format = obj.pixelFormat;
|
||||
|
||||
// First skip over the mipmap levels.
|
||||
for (uint32_t mipLvl = 0; mipLvl < p_subresource.mipmap; mipLvl++) {
|
||||
MTLSize mip_size = mipmapLevelSizeFromSize(size, mipLvl);
|
||||
size_t bytes_per_row = pf.getBytesPerRow(pixel_format, mip_size.width);
|
||||
bytes_per_row = round_up_to_alignment(bytes_per_row, row_alignment);
|
||||
size_t bytes_per_layer = pf.getBytesPerLayer(pixel_format, bytes_per_row, mip_size.height);
|
||||
offset += bytes_per_layer * mip_size.depth * array_layers;
|
||||
}
|
||||
|
||||
// Get current mipmap.
|
||||
MTLSize mip_size = mipmapLevelSizeFromSize(size, p_subresource.mipmap);
|
||||
size_t bytes_per_row = pf.getBytesPerRow(pixel_format, mip_size.width);
|
||||
bytes_per_row = round_up_to_alignment(bytes_per_row, row_alignment);
|
||||
size_t bytes_per_layer = pf.getBytesPerLayer(pixel_format, bytes_per_row, mip_size.height);
|
||||
r_layout->size = bytes_per_layer * mip_size.depth;
|
||||
r_layout->offset = offset + (r_layout->size * p_subresource.layer - 1);
|
||||
r_layout->depth_pitch = bytes_per_layer;
|
||||
r_layout->row_pitch = bytes_per_row;
|
||||
r_layout->layer_pitch = r_layout->size * array_layers;
|
||||
}
|
||||
|
||||
void RenderingDeviceDriverMetal::texture_get_copyable_layout(TextureID p_texture, const TextureSubresource &p_subresource, TextureCopyableLayout *r_layout) {
|
||||
id<MTLTexture> obj = rid::get(p_texture);
|
||||
*r_layout = {};
|
||||
|
||||
if ((obj.resourceOptions & MTLResourceStorageModePrivate) != 0) {
|
||||
MTLSize sz = MTLSizeMake(obj.width, obj.height, obj.depth);
|
||||
PixelFormats &pf = *pixel_formats;
|
||||
DataFormat format = pf.getDataFormat(obj.pixelFormat);
|
||||
|
||||
PixelFormats &pf = *pixel_formats;
|
||||
DataFormat format = pf.getDataFormat(obj.pixelFormat);
|
||||
if (p_subresource.mipmap > 0) {
|
||||
r_layout->offset = get_image_format_required_size(format, sz.width, sz.height, sz.depth, p_subresource.mipmap);
|
||||
}
|
||||
MTLSize sz = MTLSizeMake(obj.width, obj.height, obj.depth);
|
||||
|
||||
sz = mipmapLevelSizeFromSize(sz, p_subresource.mipmap);
|
||||
|
||||
uint32_t bw = 0, bh = 0;
|
||||
get_compressed_image_format_block_dimensions(format, bw, bh);
|
||||
uint32_t sbw = 0, sbh = 0;
|
||||
r_layout->size = get_image_format_required_size(format, sz.width, sz.height, sz.depth, 1, &sbw, &sbh);
|
||||
r_layout->row_pitch = r_layout->size / ((sbh / bh) * sz.depth);
|
||||
r_layout->depth_pitch = r_layout->size / sz.depth;
|
||||
|
||||
uint32_t array_length = obj.arrayLength;
|
||||
if (obj.textureType == MTLTextureTypeCube) {
|
||||
array_length = 6;
|
||||
} else if (obj.textureType == MTLTextureTypeCubeArray) {
|
||||
array_length *= 6;
|
||||
}
|
||||
r_layout->layer_pitch = r_layout->size / array_length;
|
||||
} else {
|
||||
CRASH_NOW_MSG("need to calculate layout for shared texture");
|
||||
if (p_subresource.mipmap > 0) {
|
||||
r_layout->offset = get_image_format_required_size(format, sz.width, sz.height, sz.depth, p_subresource.mipmap);
|
||||
}
|
||||
|
||||
sz = mipmapLevelSizeFromSize(sz, p_subresource.mipmap);
|
||||
|
||||
uint32_t bw = 0, bh = 0;
|
||||
get_compressed_image_format_block_dimensions(format, bw, bh);
|
||||
uint32_t sbw = 0, sbh = 0;
|
||||
r_layout->size = get_image_format_required_size(format, sz.width, sz.height, sz.depth, 1, &sbw, &sbh);
|
||||
r_layout->row_pitch = r_layout->size / ((sbh / bh) * sz.depth);
|
||||
r_layout->depth_pitch = r_layout->size / sz.depth;
|
||||
|
||||
uint32_t array_length = obj.arrayLength;
|
||||
if (obj.textureType == MTLTextureTypeCube) {
|
||||
array_length = 6;
|
||||
} else if (obj.textureType == MTLTextureTypeCubeArray) {
|
||||
array_length *= 6;
|
||||
}
|
||||
r_layout->layer_pitch = r_layout->size / array_length;
|
||||
}
|
||||
|
||||
Vector<uint8_t> RenderingDeviceDriverMetal::texture_get_data(TextureID p_texture, uint32_t p_layer) {
|
||||
id<MTLTexture> obj = rid::get(p_texture);
|
||||
ERR_FAIL_COND_V_MSG(obj.storageMode != MTLStorageModeShared, Vector<uint8_t>(), "Texture must be created with TEXTURE_USAGE_CPU_READ_BIT set.");
|
||||
|
||||
if (obj.buffer) {
|
||||
ERR_FAIL_COND_V_MSG(p_layer > 0, Vector<uint8_t>(), "A linear texture has a single layer.");
|
||||
ERR_FAIL_COND_V_MSG(obj.mipmapLevelCount > 1, Vector<uint8_t>(), "A linear texture has a single mipmap level.");
|
||||
Vector<uint8_t> image_data;
|
||||
image_data.resize_uninitialized(obj.buffer.length);
|
||||
memcpy(image_data.ptrw(), obj.buffer.contents, obj.buffer.length);
|
||||
return image_data;
|
||||
}
|
||||
|
||||
DataFormat tex_format = pixel_formats->getDataFormat(obj.pixelFormat);
|
||||
uint32_t tex_w = obj.width;
|
||||
uint32_t tex_h = obj.height;
|
||||
uint32_t tex_d = obj.depth;
|
||||
uint32_t tex_mipmaps = obj.mipmapLevelCount;
|
||||
|
||||
// Must iteratively copy the texture data to a buffer.
|
||||
|
||||
uint32_t tight_mip_size = get_image_format_required_size(tex_format, tex_w, tex_h, tex_d, tex_mipmaps);
|
||||
|
||||
Vector<uint8_t> image_data;
|
||||
image_data.resize(tight_mip_size);
|
||||
|
||||
uint32_t pixel_size = get_image_format_pixel_size(tex_format);
|
||||
uint32_t pixel_rshift = get_compressed_image_format_pixel_rshift(tex_format);
|
||||
uint32_t blockw = 0, blockh = 0;
|
||||
get_compressed_image_format_block_dimensions(tex_format, blockw, blockh);
|
||||
|
||||
uint8_t *dest_ptr = image_data.ptrw();
|
||||
|
||||
for (uint32_t mm_i = 0; mm_i < tex_mipmaps; mm_i++) {
|
||||
uint32_t bw = STEPIFY(tex_w, blockw);
|
||||
uint32_t bh = STEPIFY(tex_h, blockh);
|
||||
|
||||
uint32_t bytes_per_row = (bw * pixel_size) >> pixel_rshift;
|
||||
uint32_t bytes_per_img = bytes_per_row * bh;
|
||||
uint32_t mip_size = bytes_per_img * tex_d;
|
||||
|
||||
[obj getBytes:(void *)dest_ptr
|
||||
bytesPerRow:bytes_per_row
|
||||
bytesPerImage:bytes_per_img
|
||||
fromRegion:MTLRegionMake3D(0, 0, 0, bw, bh, tex_d)
|
||||
mipmapLevel:mm_i
|
||||
slice:p_layer];
|
||||
|
||||
dest_ptr += mip_size;
|
||||
|
||||
// Next mipmap level.
|
||||
tex_w = MAX(blockw, tex_w >> 1);
|
||||
tex_h = MAX(blockh, tex_h >> 1);
|
||||
tex_d = MAX(1u, tex_d >> 1);
|
||||
}
|
||||
|
||||
// Ensure that the destination pointer is at the end of the image data.
|
||||
DEV_ASSERT(dest_ptr - image_data.ptr() == image_data.size());
|
||||
|
||||
return image_data;
|
||||
}
|
||||
|
||||
uint8_t *RenderingDeviceDriverMetal::texture_map(TextureID p_texture, const TextureSubresource &p_subresource) {
|
||||
id<MTLTexture> obj = rid::get(p_texture);
|
||||
ERR_FAIL_NULL_V_MSG(obj.buffer, nullptr, "texture is not created from a buffer");
|
||||
ERR_FAIL_COND_V_MSG(obj.storageMode != MTLStorageModeShared, nullptr, "Texture must be created with TEXTURE_USAGE_CPU_READ_BIT set.");
|
||||
ERR_FAIL_COND_V_MSG(obj.buffer, nullptr, "Texture mapping is not supported for non-linear textures in Metal.");
|
||||
ERR_FAIL_COND_V_MSG(p_subresource.layer > 0, nullptr, "A linear texture should have a single layer.");
|
||||
ERR_FAIL_COND_V_MSG(p_subresource.mipmap > 0, nullptr, "A linear texture should have a single mipmap.");
|
||||
|
||||
TextureCopyableLayout layout;
|
||||
_get_sub_resource(p_texture, p_subresource, &layout);
|
||||
return (uint8_t *)(obj.buffer.contents) + layout.offset;
|
||||
PixelFormats &pf = *pixel_formats;
|
||||
|
||||
size_t row_alignment = get_texel_buffer_alignment_for_format(obj.pixelFormat);
|
||||
size_t offset = 0;
|
||||
size_t array_layers = obj.arrayLength;
|
||||
MTLSize size = MTLSizeMake(obj.width, obj.height, obj.depth);
|
||||
MTLPixelFormat pixel_format = obj.pixelFormat;
|
||||
|
||||
// First skip over the mipmap levels.
|
||||
for (uint32_t mipLvl = 0; mipLvl < p_subresource.mipmap; mipLvl++) {
|
||||
MTLSize mipExtent = mipmapLevelSizeFromSize(size, mipLvl);
|
||||
size_t bytes_per_row = pf.getBytesPerRow(pixel_format, mipExtent.width);
|
||||
bytes_per_row = round_up_to_alignment(bytes_per_row, row_alignment);
|
||||
size_t bytes_per_layer = pf.getBytesPerLayer(pixel_format, bytes_per_row, mipExtent.height);
|
||||
offset += bytes_per_layer * mipExtent.depth * array_layers;
|
||||
}
|
||||
|
||||
if (p_subresource.layer > 1) {
|
||||
// Calculate offset to desired layer.
|
||||
MTLSize mipExtent = mipmapLevelSizeFromSize(size, p_subresource.mipmap);
|
||||
size_t bytes_per_row = pf.getBytesPerRow(pixel_format, mipExtent.width);
|
||||
bytes_per_row = round_up_to_alignment(bytes_per_row, row_alignment);
|
||||
size_t bytes_per_layer = pf.getBytesPerLayer(pixel_format, bytes_per_row, mipExtent.height);
|
||||
offset += bytes_per_layer * mipExtent.depth * (p_subresource.layer - 1);
|
||||
}
|
||||
|
||||
// TODO: Confirm with rendering team that there is no other way Godot may attempt to map a texture with multiple mipmaps or array layers.
|
||||
|
||||
// NOTE: It is not possible to create a buffer-backed texture with mipmaps or array layers,
|
||||
// as noted in the is_valid_linear function, so the offset calculation SHOULD always be zero.
|
||||
// Given that, this code should be simplified.
|
||||
|
||||
return (uint8_t *)(obj.buffer.contents) + offset;
|
||||
return (uint8_t *)obj.buffer.contents;
|
||||
}
|
||||
|
||||
void RenderingDeviceDriverMetal::texture_unmap(TextureID p_texture) {
|
||||
|
|
|
@ -2236,6 +2236,78 @@ void RenderingDeviceDriverVulkan::texture_get_copyable_layout(TextureID p_textur
|
|||
}
|
||||
}
|
||||
|
||||
Vector<uint8_t> RenderingDeviceDriverVulkan::texture_get_data(TextureID p_texture, uint32_t p_layer) {
|
||||
const TextureInfo *tex = (const TextureInfo *)p_texture.id;
|
||||
|
||||
DataFormat tex_format = tex->rd_format;
|
||||
uint32_t tex_width = tex->vk_create_info.extent.width;
|
||||
uint32_t tex_height = tex->vk_create_info.extent.height;
|
||||
uint32_t tex_depth = tex->vk_create_info.extent.depth;
|
||||
uint32_t tex_mipmaps = tex->vk_create_info.mipLevels;
|
||||
|
||||
uint32_t width, height, depth;
|
||||
uint32_t tight_mip_size = get_image_format_required_size(tex_format, tex_width, tex_height, tex_depth, tex_mipmaps, &width, &height, &depth);
|
||||
|
||||
Vector<uint8_t> image_data;
|
||||
image_data.resize(tight_mip_size);
|
||||
|
||||
uint32_t blockw, blockh;
|
||||
get_compressed_image_format_block_dimensions(tex_format, blockw, blockh);
|
||||
uint32_t block_size = get_compressed_image_format_block_byte_size(tex_format);
|
||||
uint32_t pixel_size = get_image_format_pixel_size(tex_format);
|
||||
|
||||
{
|
||||
uint8_t *w = image_data.ptrw();
|
||||
|
||||
uint32_t mipmap_offset = 0;
|
||||
for (uint32_t mm_i = 0; mm_i < tex_mipmaps; mm_i++) {
|
||||
uint32_t image_total = get_image_format_required_size(tex_format, tex_width, tex_height, tex_depth, mm_i + 1, &width, &height, &depth);
|
||||
|
||||
uint8_t *write_ptr_mipmap = w + mipmap_offset;
|
||||
tight_mip_size = image_total - mipmap_offset;
|
||||
|
||||
RDD::TextureSubresource subres;
|
||||
subres.aspect = RDD::TEXTURE_ASPECT_COLOR;
|
||||
subres.layer = p_layer;
|
||||
subres.mipmap = mm_i;
|
||||
RDD::TextureCopyableLayout layout;
|
||||
texture_get_copyable_layout(p_texture, subres, &layout);
|
||||
|
||||
uint8_t *img_mem = texture_map(p_texture, subres);
|
||||
ERR_FAIL_NULL_V(img_mem, Vector<uint8_t>());
|
||||
|
||||
for (uint32_t z = 0; z < depth; z++) {
|
||||
uint8_t *write_ptr = write_ptr_mipmap + z * tight_mip_size / depth;
|
||||
const uint8_t *slice_read_ptr = img_mem + z * layout.depth_pitch;
|
||||
|
||||
if (block_size > 1) {
|
||||
// Compressed.
|
||||
uint32_t line_width = (block_size * (width / blockw));
|
||||
for (uint32_t y = 0; y < height / blockh; y++) {
|
||||
const uint8_t *rptr = slice_read_ptr + y * layout.row_pitch;
|
||||
uint8_t *wptr = write_ptr + y * line_width;
|
||||
|
||||
memcpy(wptr, rptr, line_width);
|
||||
}
|
||||
} else {
|
||||
// Uncompressed.
|
||||
for (uint32_t y = 0; y < height; y++) {
|
||||
const uint8_t *rptr = slice_read_ptr + y * layout.row_pitch;
|
||||
uint8_t *wptr = write_ptr + y * pixel_size * width;
|
||||
memcpy(wptr, rptr, (uint64_t)pixel_size * width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
texture_unmap(p_texture);
|
||||
|
||||
mipmap_offset = image_total;
|
||||
}
|
||||
}
|
||||
|
||||
return image_data;
|
||||
}
|
||||
|
||||
uint8_t *RenderingDeviceDriverVulkan::texture_map(TextureID p_texture, const TextureSubresource &p_subresource) {
|
||||
const TextureInfo *tex_info = (const TextureInfo *)p_texture.id;
|
||||
|
||||
|
|
|
@ -234,6 +234,7 @@ public:
|
|||
virtual void texture_free(TextureID p_texture) override final;
|
||||
virtual uint64_t texture_get_allocation_size(TextureID p_texture) override final;
|
||||
virtual void texture_get_copyable_layout(TextureID p_texture, const TextureSubresource &p_subresource, TextureCopyableLayout *r_layout) override final;
|
||||
virtual Vector<uint8_t> texture_get_data(TextureID p_texture, uint32_t p_layer) override final;
|
||||
virtual uint8_t *texture_map(TextureID p_texture, const TextureSubresource &p_subresource) override final;
|
||||
virtual void texture_unmap(TextureID p_texture) override final;
|
||||
virtual BitField<TextureUsageBits> texture_get_usages_supported_by_format(DataFormat p_format, bool p_cpu_readable) override final;
|
||||
|
|
|
@ -71,7 +71,7 @@ static String _get_indent(const String &p_text) {
|
|||
static String _translate_doc_string(const String &p_text) {
|
||||
const String indent = _get_indent(p_text);
|
||||
const String message = p_text.dedent().strip_edges();
|
||||
const String translated = TranslationServer::get_singleton()->doc_translate(message, "");
|
||||
const String translated = TranslationServer::get_singleton()->get_doc_domain()->translate(message, StringName());
|
||||
// No need to restore stripped edges because they'll be stripped again later.
|
||||
return translated.indent(indent);
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ inline constexpr const unsigned char _{category}_translation_{name}_compressed[]
|
|||
file.write(f"""\
|
||||
#include "{target_h}"
|
||||
|
||||
const {category.capitalize()}TranslationList _{category}_translations[] = {{
|
||||
const EditorTranslationList _{category}_translations[] = {{
|
||||
""")
|
||||
|
||||
for x in xl_names:
|
||||
|
@ -137,12 +137,18 @@ const {category.capitalize()}TranslationList _{category}_translations[] = {{
|
|||
|
||||
with methods.generated_wrapper(target_h) as file:
|
||||
file.write(f"""\
|
||||
struct {category.capitalize()}TranslationList {{
|
||||
|
||||
#ifndef EDITOR_TRANSLATION_LIST
|
||||
#define EDITOR_TRANSLATION_LIST
|
||||
|
||||
struct EditorTranslationList {{
|
||||
const char* lang;
|
||||
int comp_size;
|
||||
int uncomp_size;
|
||||
const unsigned char* data;
|
||||
}};
|
||||
|
||||
extern const {category.capitalize()}TranslationList _{category}_translations[];
|
||||
#endif // EDITOR_TRANSLATION_LIST
|
||||
|
||||
extern const EditorTranslationList _{category}_translations[];
|
||||
""")
|
||||
|
|
|
@ -581,7 +581,7 @@ void EditorNode::_update_translations() {
|
|||
if (main->is_enabled()) {
|
||||
// Check for the exact locale.
|
||||
// `get_potential_translations("zh_CN")` could return translations for "zh".
|
||||
if (main->get_loaded_locales().has(main->get_locale_override())) {
|
||||
if (main->has_translation_for_locale(main->get_locale_override())) {
|
||||
// The set of translation resources for the current locale changed.
|
||||
const HashSet<Ref<Translation>> translations = main->get_potential_translations(main->get_locale_override());
|
||||
if (translations != tracked_translations) {
|
||||
|
|
|
@ -120,7 +120,7 @@ String EditorPropertyNameProcessor::process_name(const String &p_name, Style p_s
|
|||
case STYLE_LOCALIZED: {
|
||||
const String capitalized = _capitalize_name(p_name);
|
||||
if (TranslationServer::get_singleton()) {
|
||||
return TranslationServer::get_singleton()->property_translate(capitalized, _get_context(p_name, p_property, p_class));
|
||||
return TranslationServer::get_singleton()->get_property_domain()->translate(capitalized, _get_context(p_name, p_property, p_class));
|
||||
}
|
||||
return capitalized;
|
||||
} break;
|
||||
|
@ -130,7 +130,7 @@ String EditorPropertyNameProcessor::process_name(const String &p_name, Style p_s
|
|||
|
||||
String EditorPropertyNameProcessor::translate_group_name(const String &p_name) const {
|
||||
if (TranslationServer::get_singleton()) {
|
||||
return TranslationServer::get_singleton()->property_translate(p_name);
|
||||
return TranslationServer::get_singleton()->get_property_domain()->translate(p_name, StringName());
|
||||
}
|
||||
return p_name;
|
||||
}
|
||||
|
|
|
@ -1337,16 +1337,10 @@ void EditorSettings::setup_language(bool p_initial_setup) {
|
|||
TranslationServer::get_singleton()->set_locale(lang);
|
||||
return; // Default, nothing to do.
|
||||
}
|
||||
// Load editor translation for configured/detected locale.
|
||||
|
||||
load_editor_translations(lang);
|
||||
load_property_translations(lang);
|
||||
|
||||
// Load class reference translation.
|
||||
load_doc_translations(lang);
|
||||
|
||||
// Load extractable translation for projects.
|
||||
load_extractable_translations(lang);
|
||||
|
||||
TranslationServer::get_singleton()->set_locale(lang);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,26 +42,20 @@
|
|||
Vector<String> get_editor_locales() {
|
||||
Vector<String> locales;
|
||||
|
||||
const EditorTranslationList *etl = _editor_translations;
|
||||
while (etl->data) {
|
||||
for (const EditorTranslationList *etl = _editor_translations; etl->data; etl++) {
|
||||
const String &locale = etl->lang;
|
||||
locales.push_back(locale);
|
||||
|
||||
etl++;
|
||||
}
|
||||
|
||||
return locales;
|
||||
}
|
||||
|
||||
void load_editor_translations(const String &p_locale) {
|
||||
const Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_or_add_domain("godot.editor");
|
||||
|
||||
const EditorTranslationList *etl = _editor_translations;
|
||||
while (etl->data) {
|
||||
static void _load(const Ref<TranslationDomain> p_domain, const String &p_locale, const EditorTranslationList *p_etl) {
|
||||
for (const EditorTranslationList *etl = p_etl; etl->data; etl++) {
|
||||
if (etl->lang == p_locale) {
|
||||
Vector<uint8_t> data;
|
||||
data.resize(etl->uncomp_size);
|
||||
const int64_t ret = Compression::decompress(data.ptrw(), etl->uncomp_size, etl->data, etl->comp_size, Compression::MODE_DEFLATE);
|
||||
LocalVector<uint8_t> data;
|
||||
data.resize_uninitialized(etl->uncomp_size);
|
||||
const int64_t ret = Compression::decompress(data.ptr(), etl->uncomp_size, etl->data, etl->comp_size, Compression::MODE_DEFLATE);
|
||||
ERR_FAIL_COND_MSG(ret == -1, "Compressed file is corrupt.");
|
||||
|
||||
Ref<FileAccessMemory> fa;
|
||||
|
@ -69,115 +63,45 @@ void load_editor_translations(const String &p_locale) {
|
|||
fa->open_custom(data.ptr(), data.size());
|
||||
|
||||
Ref<Translation> tr = TranslationLoaderPO::load_translation(fa);
|
||||
|
||||
if (tr.is_valid()) {
|
||||
tr->set_locale(etl->lang);
|
||||
domain->add_translation(tr);
|
||||
p_domain->add_translation(tr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
etl++;
|
||||
}
|
||||
}
|
||||
|
||||
void load_property_translations(const String &p_locale) {
|
||||
const Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_or_add_domain("godot.properties");
|
||||
void load_editor_translations(const String &p_locale) {
|
||||
Ref<TranslationDomain> domain;
|
||||
|
||||
const PropertyTranslationList *etl = _property_translations;
|
||||
while (etl->data) {
|
||||
if (etl->lang == p_locale) {
|
||||
Vector<uint8_t> data;
|
||||
data.resize(etl->uncomp_size);
|
||||
const int64_t ret = Compression::decompress(data.ptrw(), etl->uncomp_size, etl->data, etl->comp_size, Compression::MODE_DEFLATE);
|
||||
ERR_FAIL_COND_MSG(ret == -1, "Compressed file is corrupt.");
|
||||
domain = TranslationServer::get_singleton()->get_editor_domain();
|
||||
domain->clear();
|
||||
_load(domain, p_locale, _editor_translations);
|
||||
_load(domain, p_locale, _extractable_translations);
|
||||
|
||||
Ref<FileAccessMemory> fa;
|
||||
fa.instantiate();
|
||||
fa->open_custom(data.ptr(), data.size());
|
||||
|
||||
Ref<Translation> tr = TranslationLoaderPO::load_translation(fa);
|
||||
|
||||
if (tr.is_valid()) {
|
||||
tr->set_locale(etl->lang);
|
||||
domain->add_translation(tr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
etl++;
|
||||
}
|
||||
domain = TranslationServer::get_singleton()->get_property_domain();
|
||||
domain->clear();
|
||||
_load(domain, p_locale, _property_translations);
|
||||
}
|
||||
|
||||
void load_doc_translations(const String &p_locale) {
|
||||
const Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_or_add_domain("godot.documentation");
|
||||
|
||||
const DocTranslationList *dtl = _doc_translations;
|
||||
while (dtl->data) {
|
||||
if (dtl->lang == p_locale) {
|
||||
Vector<uint8_t> data;
|
||||
data.resize(dtl->uncomp_size);
|
||||
const int64_t ret = Compression::decompress(data.ptrw(), dtl->uncomp_size, dtl->data, dtl->comp_size, Compression::MODE_DEFLATE);
|
||||
ERR_FAIL_COND_MSG(ret == -1, "Compressed file is corrupt.");
|
||||
|
||||
Ref<FileAccessMemory> fa;
|
||||
fa.instantiate();
|
||||
fa->open_custom(data.ptr(), data.size());
|
||||
|
||||
Ref<Translation> tr = TranslationLoaderPO::load_translation(fa);
|
||||
|
||||
if (tr.is_valid()) {
|
||||
tr->set_locale(dtl->lang);
|
||||
domain->add_translation(tr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dtl++;
|
||||
}
|
||||
}
|
||||
|
||||
void load_extractable_translations(const String &p_locale) {
|
||||
const Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_or_add_domain("godot.editor");
|
||||
|
||||
const ExtractableTranslationList *etl = _extractable_translations;
|
||||
while (etl->data) {
|
||||
if (etl->lang == p_locale) {
|
||||
Vector<uint8_t> data;
|
||||
data.resize(etl->uncomp_size);
|
||||
const int64_t ret = Compression::decompress(data.ptrw(), etl->uncomp_size, etl->data, etl->comp_size, Compression::MODE_DEFLATE);
|
||||
ERR_FAIL_COND_MSG(ret == -1, "Compressed file is corrupt.");
|
||||
|
||||
Ref<FileAccessMemory> fa;
|
||||
fa.instantiate();
|
||||
fa->open_custom(data.ptr(), data.size());
|
||||
|
||||
Ref<Translation> tr = TranslationLoaderPO::load_translation(fa);
|
||||
|
||||
if (tr.is_valid()) {
|
||||
tr->set_locale(etl->lang);
|
||||
domain->add_translation(tr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
etl++;
|
||||
}
|
||||
const Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_doc_domain();
|
||||
domain->clear();
|
||||
_load(domain, p_locale, _doc_translations);
|
||||
}
|
||||
|
||||
Vector<Vector<String>> get_extractable_message_list() {
|
||||
const ExtractableTranslationList *etl = _extractable_translations;
|
||||
Vector<Vector<String>> list;
|
||||
|
||||
while (etl->data) {
|
||||
for (const EditorTranslationList *etl = _extractable_translations; etl->data; etl++) {
|
||||
if (strcmp(etl->lang, "source")) {
|
||||
etl++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector<uint8_t> data;
|
||||
data.resize(etl->uncomp_size);
|
||||
const int64_t ret = Compression::decompress(data.ptrw(), etl->uncomp_size, etl->data, etl->comp_size, Compression::MODE_DEFLATE);
|
||||
LocalVector<uint8_t> data;
|
||||
data.resize_uninitialized(etl->uncomp_size);
|
||||
const int64_t ret = Compression::decompress(data.ptr(), etl->uncomp_size, etl->data, etl->comp_size, Compression::MODE_DEFLATE);
|
||||
ERR_FAIL_COND_V_MSG(ret == -1, list, "Compressed file is corrupt.");
|
||||
|
||||
Ref<FileAccessMemory> fa;
|
||||
|
@ -322,8 +246,6 @@ Vector<Vector<String>> get_extractable_message_list() {
|
|||
line++;
|
||||
}
|
||||
}
|
||||
|
||||
etl++;
|
||||
}
|
||||
|
||||
return list;
|
||||
|
|
|
@ -35,7 +35,5 @@
|
|||
|
||||
Vector<String> get_editor_locales();
|
||||
void load_editor_translations(const String &p_locale);
|
||||
void load_property_translations(const String &p_locale);
|
||||
void load_doc_translations(const String &p_locale);
|
||||
void load_extractable_translations(const String &p_locale);
|
||||
Vector<Vector<String>> get_extractable_message_list();
|
||||
|
|
|
@ -2647,9 +2647,11 @@ void DisplayServerMacOS::window_set_flag(WindowFlags p_flag, bool p_enabled, Win
|
|||
[wd.window_object setBackgroundColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.004f]];
|
||||
}
|
||||
// Force update of the window styles.
|
||||
NSRect frameRect = [wd.window_object frame];
|
||||
[wd.window_object setFrame:NSMakeRect(frameRect.origin.x, frameRect.origin.y, frameRect.size.width + 1, frameRect.size.height) display:NO];
|
||||
[wd.window_object setFrame:frameRect display:NO];
|
||||
if ([wd.window_object isVisible]) {
|
||||
NSRect frameRect = [wd.window_object frame];
|
||||
[wd.window_object setFrame:NSMakeRect(frameRect.origin.x, frameRect.origin.y, frameRect.size.width + 1, frameRect.size.height) display:NO];
|
||||
[wd.window_object setFrame:frameRect display:NO];
|
||||
}
|
||||
}
|
||||
_update_window_style(wd, p_window);
|
||||
if (was_visible || [wd.window_object isVisible]) {
|
||||
|
@ -2683,9 +2685,11 @@ void DisplayServerMacOS::window_set_flag(WindowFlags p_flag, bool p_enabled, Win
|
|||
wd.layered_window = p_enabled;
|
||||
set_window_per_pixel_transparency_enabled(p_enabled, p_window);
|
||||
// Force update of the window styles.
|
||||
NSRect frameRect = [wd.window_object frame];
|
||||
[wd.window_object setFrame:NSMakeRect(frameRect.origin.x, frameRect.origin.y, frameRect.size.width + 1, frameRect.size.height) display:NO];
|
||||
[wd.window_object setFrame:frameRect display:NO];
|
||||
if ([wd.window_object isVisible]) {
|
||||
NSRect frameRect = [wd.window_object frame];
|
||||
[wd.window_object setFrame:NSMakeRect(frameRect.origin.x, frameRect.origin.y, frameRect.size.width + 1, frameRect.size.height) display:NO];
|
||||
[wd.window_object setFrame:frameRect display:NO];
|
||||
}
|
||||
} break;
|
||||
case WINDOW_FLAG_NO_FOCUS: {
|
||||
wd.no_focus = p_enabled;
|
||||
|
|
|
@ -1118,7 +1118,7 @@ bool Viewport::_set_size(const Size2i &p_size, const Size2 &p_size_2d_override,
|
|||
stretch_transform = stretch_transform_new;
|
||||
font_oversampling = new_font_oversampling;
|
||||
|
||||
#ifndef _3D_DISABLED
|
||||
#ifndef XR_DISABLED
|
||||
if (!use_xr) {
|
||||
#endif
|
||||
|
||||
|
@ -1128,7 +1128,7 @@ bool Viewport::_set_size(const Size2i &p_size, const Size2 &p_size_2d_override,
|
|||
RS::get_singleton()->viewport_set_size(viewport, 0, 0);
|
||||
}
|
||||
|
||||
#ifndef _3D_DISABLED
|
||||
#ifndef XR_DISABLED
|
||||
} // if (!use_xr)
|
||||
#endif
|
||||
|
||||
|
|
|
@ -844,7 +844,7 @@ void RenderForwardClustered::_fill_instance_data(RenderListType p_render_list, i
|
|||
|
||||
RenderElementInfo &element_info = rl->element_info[p_offset + i];
|
||||
|
||||
element_info.value = uint32_t((surface->sort.sort_key2 & 0x0FFF00000000) >> 32u);
|
||||
element_info.value = uint32_t(surface->sort.sort_key1 & 0xFFF);
|
||||
|
||||
if (cant_repeat) {
|
||||
prev_surface = nullptr;
|
||||
|
@ -4075,7 +4075,8 @@ void RenderForwardClustered::_geometry_instance_add_surface_with_material(Geomet
|
|||
sdcache->sort.sort_key2 = 0;
|
||||
|
||||
sdcache->sort.surface_index = p_surface;
|
||||
sdcache->sort.material_id = p_material_id;
|
||||
sdcache->sort.material_id_hi = (p_material_id & 0xFF000000) >> 24;
|
||||
sdcache->sort.material_id_lo = (p_material_id & 0x00FFFFFF);
|
||||
sdcache->sort.shader_id = p_shader_id;
|
||||
sdcache->sort.geometry_id = p_mesh.get_local_index(); //only meshes can repeat anyway
|
||||
sdcache->sort.uses_forward_gi = ginstance->can_sdfgi;
|
||||
|
|
|
@ -498,17 +498,23 @@ private:
|
|||
uint64_t sort_key2;
|
||||
};
|
||||
struct {
|
||||
uint64_t geometry_id : 32;
|
||||
uint64_t material_id : 32;
|
||||
|
||||
uint64_t shader_id : 32;
|
||||
// Needs to be grouped together to be used in RenderElementInfo, as the value is masked directly.
|
||||
uint64_t lod_index : 8;
|
||||
uint64_t uses_softshadow : 1;
|
||||
uint64_t uses_projector : 1;
|
||||
uint64_t uses_forward_gi : 1;
|
||||
uint64_t uses_lightmap : 1;
|
||||
|
||||
// Sorted based on optimal order for respecting priority and reducing the amount of rebinding of shaders, materials,
|
||||
// and geometry. This current order was found to be the most optimal in large projects. If you wish to measure
|
||||
// differences, refer to RenderingDeviceGraph and the methods available to print statistics for draw lists.
|
||||
uint64_t depth_layer : 4;
|
||||
uint64_t surface_index : 8;
|
||||
uint64_t geometry_id : 32;
|
||||
uint64_t material_id_hi : 8;
|
||||
|
||||
uint64_t material_id_lo : 24;
|
||||
uint64_t shader_id : 32;
|
||||
uint64_t priority : 8;
|
||||
};
|
||||
} sort;
|
||||
|
|
|
@ -1981,7 +1981,7 @@ void RenderForwardMobile::_fill_instance_data(RenderListType p_render_list, uint
|
|||
RenderElementInfo &element_info = rl->element_info[p_offset + i];
|
||||
|
||||
// Sets lod_index and uses_lightmap at once.
|
||||
element_info.value = uint32_t((surface->sort.sort_key2 & 0x01FF00000000) >> 32u);
|
||||
element_info.value = uint32_t(surface->sort.sort_key1 & 0x1FF);
|
||||
}
|
||||
|
||||
if (p_update_buffer) {
|
||||
|
@ -2764,7 +2764,8 @@ void RenderForwardMobile::_geometry_instance_add_surface_with_material(GeometryI
|
|||
sdcache->sort.sort_key2 = 0;
|
||||
|
||||
sdcache->sort.surface_index = p_surface;
|
||||
sdcache->sort.material_id = p_material_id;
|
||||
sdcache->sort.material_id_hi = (p_material_id & 0xFF000000) >> 24;
|
||||
sdcache->sort.material_id_lo = (p_material_id & 0x00FFFFFF);
|
||||
sdcache->sort.shader_id = p_shader_id;
|
||||
sdcache->sort.geometry_id = p_mesh.get_local_index();
|
||||
sdcache->sort.priority = p_material->priority;
|
||||
|
|
|
@ -478,15 +478,21 @@ protected:
|
|||
uint64_t sort_key2;
|
||||
};
|
||||
struct {
|
||||
uint64_t geometry_id : 32;
|
||||
uint64_t material_id : 32;
|
||||
|
||||
uint64_t shader_id : 32;
|
||||
// Needs to be grouped together to be used in RenderElementInfo, as the value is masked directly.
|
||||
uint64_t lod_index : 8;
|
||||
uint64_t uses_lightmap : 1;
|
||||
uint64_t pad : 3;
|
||||
|
||||
// Sorted based on optimal order for respecting priority and reducing the amount of rebinding of shaders, materials,
|
||||
// and geometry. This current order was found to be the most optimal in large projects. If you wish to measure
|
||||
// differences, refer to RenderingDeviceGraph and the methods available to print statistics for draw lists.
|
||||
uint64_t depth_layer : 4;
|
||||
uint64_t surface_index : 8;
|
||||
uint64_t geometry_id : 32;
|
||||
uint64_t material_id_hi : 8;
|
||||
|
||||
uint64_t material_id_lo : 24;
|
||||
uint64_t shader_id : 32;
|
||||
uint64_t priority : 8;
|
||||
};
|
||||
} sort;
|
||||
|
|
|
@ -1903,71 +1903,6 @@ uint32_t RenderingDevice::_texture_vrs_method_to_usage_bits() const {
|
|||
}
|
||||
}
|
||||
|
||||
Vector<uint8_t> RenderingDevice::_texture_get_data(Texture *tex, uint32_t p_layer, bool p_2d) {
|
||||
uint32_t width, height, depth;
|
||||
uint32_t tight_mip_size = get_image_format_required_size(tex->format, tex->width, tex->height, p_2d ? 1 : tex->depth, tex->mipmaps, &width, &height, &depth);
|
||||
|
||||
Vector<uint8_t> image_data;
|
||||
image_data.resize(tight_mip_size);
|
||||
|
||||
uint32_t blockw, blockh;
|
||||
get_compressed_image_format_block_dimensions(tex->format, blockw, blockh);
|
||||
uint32_t block_size = get_compressed_image_format_block_byte_size(tex->format);
|
||||
uint32_t pixel_size = get_image_format_pixel_size(tex->format);
|
||||
|
||||
{
|
||||
uint8_t *w = image_data.ptrw();
|
||||
|
||||
uint32_t mipmap_offset = 0;
|
||||
for (uint32_t mm_i = 0; mm_i < tex->mipmaps; mm_i++) {
|
||||
uint32_t image_total = get_image_format_required_size(tex->format, tex->width, tex->height, p_2d ? 1 : tex->depth, mm_i + 1, &width, &height, &depth);
|
||||
|
||||
uint8_t *write_ptr_mipmap = w + mipmap_offset;
|
||||
tight_mip_size = image_total - mipmap_offset;
|
||||
|
||||
RDD::TextureSubresource subres;
|
||||
subres.aspect = RDD::TEXTURE_ASPECT_COLOR;
|
||||
subres.layer = p_layer;
|
||||
subres.mipmap = mm_i;
|
||||
RDD::TextureCopyableLayout layout;
|
||||
driver->texture_get_copyable_layout(tex->driver_id, subres, &layout);
|
||||
|
||||
uint8_t *img_mem = driver->texture_map(tex->driver_id, subres);
|
||||
ERR_FAIL_NULL_V(img_mem, Vector<uint8_t>());
|
||||
|
||||
for (uint32_t z = 0; z < depth; z++) {
|
||||
uint8_t *write_ptr = write_ptr_mipmap + z * tight_mip_size / depth;
|
||||
const uint8_t *slice_read_ptr = img_mem + z * layout.depth_pitch;
|
||||
|
||||
if (block_size > 1) {
|
||||
// Compressed.
|
||||
uint32_t line_width = (block_size * (width / blockw));
|
||||
for (uint32_t y = 0; y < height / blockh; y++) {
|
||||
const uint8_t *rptr = slice_read_ptr + y * layout.row_pitch;
|
||||
uint8_t *wptr = write_ptr + y * line_width;
|
||||
|
||||
memcpy(wptr, rptr, line_width);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Uncompressed.
|
||||
for (uint32_t y = 0; y < height; y++) {
|
||||
const uint8_t *rptr = slice_read_ptr + y * layout.row_pitch;
|
||||
uint8_t *wptr = write_ptr + y * pixel_size * width;
|
||||
memcpy(wptr, rptr, (uint64_t)pixel_size * width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
driver->texture_unmap(tex->driver_id);
|
||||
|
||||
mipmap_offset = image_total;
|
||||
}
|
||||
}
|
||||
|
||||
return image_data;
|
||||
}
|
||||
|
||||
Vector<uint8_t> RenderingDevice::texture_get_data(RID p_texture, uint32_t p_layer) {
|
||||
ERR_RENDER_THREAD_GUARD_V(Vector<uint8_t>());
|
||||
|
||||
|
@ -1984,8 +1919,7 @@ Vector<uint8_t> RenderingDevice::texture_get_data(RID p_texture, uint32_t p_laye
|
|||
_check_transfer_worker_texture(tex);
|
||||
|
||||
if (tex->usage_flags & TEXTURE_USAGE_CPU_READ_BIT) {
|
||||
// Does not need anything fancy, map and read.
|
||||
return _texture_get_data(tex, p_layer);
|
||||
return driver->texture_get_data(tex->driver_id, p_layer);
|
||||
} else {
|
||||
LocalVector<RDD::TextureCopyableLayout> mip_layouts;
|
||||
uint32_t work_mip_alignment = driver->api_trait_get(RDD::API_TRAIT_TEXTURE_TRANSFER_ALIGNMENT);
|
||||
|
|
|
@ -347,7 +347,6 @@ public:
|
|||
uint32_t texture_upload_region_size_px = 0;
|
||||
uint32_t texture_download_region_size_px = 0;
|
||||
|
||||
Vector<uint8_t> _texture_get_data(Texture *tex, uint32_t p_layer, bool p_2d = false);
|
||||
uint32_t _texture_layer_count(Texture *p_texture) const;
|
||||
uint32_t _texture_alignment(Texture *p_texture) const;
|
||||
Error _texture_initialize(RID p_texture, uint32_t p_layer, const Vector<uint8_t> &p_data, RDD::TextureLayout p_dst_layout, bool p_immediate_flush);
|
||||
|
|
|
@ -271,6 +271,8 @@ public:
|
|||
virtual void texture_free(TextureID p_texture) = 0;
|
||||
virtual uint64_t texture_get_allocation_size(TextureID p_texture) = 0;
|
||||
virtual void texture_get_copyable_layout(TextureID p_texture, const TextureSubresource &p_subresource, TextureCopyableLayout *r_layout) = 0;
|
||||
// Returns the data of a texture layer for a CPU texture that was created with TEXTURE_USAGE_CPU_READ_BIT.
|
||||
virtual Vector<uint8_t> texture_get_data(TextureID p_texture, uint32_t p_layer) = 0;
|
||||
virtual uint8_t *texture_map(TextureID p_texture, const TextureSubresource &p_subresource) = 0;
|
||||
virtual void texture_unmap(TextureID p_texture) = 0;
|
||||
virtual BitField<TextureUsageBits> texture_get_usages_supported_by_format(DataFormat p_format, bool p_cpu_readable) = 0;
|
||||
|
|
|
@ -35,6 +35,9 @@
|
|||
#define PRINT_RESOURCE_TRACKER_TOTAL 0
|
||||
#define PRINT_COMMAND_RECORDING 0
|
||||
|
||||
// Prints the total number of bytes used for draw lists in a frame.
|
||||
#define PRINT_DRAW_LIST_STATS 0
|
||||
|
||||
RenderingDeviceGraph::RenderingDeviceGraph() {
|
||||
driver_honors_barriers = false;
|
||||
driver_clears_with_copy_engine = false;
|
||||
|
@ -835,7 +838,15 @@ void RenderingDeviceGraph::_get_draw_list_render_pass_and_framebuffer(const Reco
|
|||
r_framebuffer = it->value.framebuffer;
|
||||
}
|
||||
|
||||
#if PRINT_DRAW_LIST_STATS
|
||||
static uint32_t draw_list_total_size = 0;
|
||||
#endif
|
||||
|
||||
void RenderingDeviceGraph::_run_draw_list_command(RDD::CommandBufferID p_command_buffer, const uint8_t *p_instruction_data, uint32_t p_instruction_data_size) {
|
||||
#if PRINT_DRAW_LIST_STATS
|
||||
draw_list_total_size += p_instruction_data_size;
|
||||
#endif
|
||||
|
||||
uint32_t instruction_data_cursor = 0;
|
||||
while (instruction_data_cursor < p_instruction_data_size) {
|
||||
DEV_ASSERT((instruction_data_cursor + sizeof(DrawListInstruction)) <= p_instruction_data_size);
|
||||
|
@ -2366,6 +2377,10 @@ void RenderingDeviceGraph::end(bool p_reorder_commands, bool p_full_barriers, RD
|
|||
workarounds_state.draw_list_found = false;
|
||||
}
|
||||
|
||||
#if PRINT_DRAW_LIST_STATS
|
||||
draw_list_total_size = 0;
|
||||
#endif
|
||||
|
||||
if (p_reorder_commands) {
|
||||
#if PRINT_RENDER_GRAPH
|
||||
print_line("BEFORE SORT");
|
||||
|
@ -2416,6 +2431,9 @@ void RenderingDeviceGraph::end(bool p_reorder_commands, bool p_full_barriers, RD
|
|||
|
||||
_run_label_command_change(r_command_buffer, -1, -1, false, false, nullptr, 0, current_label_index, current_label_level);
|
||||
|
||||
#if PRINT_DRAW_LIST_STATS
|
||||
print_line(vformat("Draw list %d bytes", draw_list_total_size));
|
||||
#endif
|
||||
#if PRINT_COMMAND_RECORDING
|
||||
print_line(vformat("Recorded %d commands", command_count));
|
||||
#endif
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
#pragma once
|
||||
|
||||
#include "core/string/optimized_translation.h"
|
||||
#include "core/string/plural_rules.h"
|
||||
#include "core/string/translation.h"
|
||||
#include "core/string/translation_po.h"
|
||||
#include "core/string/translation_server.h"
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
|
@ -45,7 +45,8 @@
|
|||
namespace TestTranslation {
|
||||
|
||||
TEST_CASE("[Translation] Messages") {
|
||||
Ref<Translation> translation = memnew(Translation);
|
||||
Ref<Translation> translation;
|
||||
translation.instantiate();
|
||||
translation->set_locale("fr");
|
||||
translation->add_message("Hello", "Bonjour");
|
||||
CHECK(translation->get_message("Hello") == "Bonjour");
|
||||
|
@ -70,8 +71,9 @@ TEST_CASE("[Translation] Messages") {
|
|||
CHECK(messages.find("Hello3"));
|
||||
}
|
||||
|
||||
TEST_CASE("[TranslationPO] Messages with context") {
|
||||
Ref<TranslationPO> translation = memnew(TranslationPO);
|
||||
TEST_CASE("[Translation] Messages with context") {
|
||||
Ref<Translation> translation;
|
||||
translation.instantiate();
|
||||
translation->set_locale("fr");
|
||||
translation->add_message("Hello", "Bonjour");
|
||||
translation->add_message("Hello", "Salut", "friendly");
|
||||
|
@ -89,11 +91,8 @@ TEST_CASE("[TranslationPO] Messages with context") {
|
|||
List<StringName> messages;
|
||||
translation->get_message_list(&messages);
|
||||
|
||||
// `get_message_count()` takes all contexts into account.
|
||||
CHECK(translation->get_message_count() == 1);
|
||||
// Only the default context is taken into account.
|
||||
// Since "Hello" is now only present in a non-default context, it is not counted in the list of messages.
|
||||
CHECK(messages.size() == 0);
|
||||
CHECK(messages.size() == 1);
|
||||
|
||||
translation->add_message("Hello2", "Bonjour2");
|
||||
translation->add_message("Hello2", "Salut2", "friendly");
|
||||
|
@ -101,32 +100,96 @@ TEST_CASE("[TranslationPO] Messages with context") {
|
|||
messages.clear();
|
||||
translation->get_message_list(&messages);
|
||||
|
||||
// `get_message_count()` takes all contexts into account.
|
||||
CHECK(translation->get_message_count() == 4);
|
||||
// Only the default context is taken into account.
|
||||
CHECK(messages.size() == 2);
|
||||
CHECK(messages.size() == 4);
|
||||
// Messages are stored in a Map, don't assume ordering.
|
||||
CHECK(messages.find("Hello2"));
|
||||
CHECK(messages.find("Hello3"));
|
||||
// Context and untranslated string are separated by EOT.
|
||||
CHECK(messages.find("friendly\x04Hello2"));
|
||||
}
|
||||
|
||||
TEST_CASE("[TranslationPO] Plural messages") {
|
||||
Ref<TranslationPO> translation = memnew(TranslationPO);
|
||||
translation->set_locale("fr");
|
||||
translation->set_plural_rule("Plural-Forms: nplurals=2; plural=(n >= 2);");
|
||||
CHECK(translation->get_plural_forms() == 2);
|
||||
TEST_CASE("[Translation] Plural messages") {
|
||||
{
|
||||
Ref<Translation> translation;
|
||||
translation.instantiate();
|
||||
translation->set_locale("fr");
|
||||
CHECK(translation->get_nplurals() == 3);
|
||||
}
|
||||
|
||||
PackedStringArray plurals;
|
||||
plurals.push_back("Il y a %d pomme");
|
||||
plurals.push_back("Il y a %d pommes");
|
||||
translation->add_plural_message("There are %d apples", plurals);
|
||||
{
|
||||
Ref<Translation> translation;
|
||||
translation.instantiate();
|
||||
translation->set_locale("invalid");
|
||||
CHECK(translation->get_nplurals() == 2);
|
||||
}
|
||||
|
||||
{
|
||||
Ref<Translation> translation;
|
||||
translation.instantiate();
|
||||
translation->set_plural_rules_override("Plural-Forms: nplurals=2; plural=(n >= 2);");
|
||||
CHECK(translation->get_nplurals() == 2);
|
||||
|
||||
PackedStringArray plurals;
|
||||
plurals.push_back("Il y a %d pomme");
|
||||
plurals.push_back("Il y a %d pommes");
|
||||
translation->add_plural_message("There are %d apples", plurals);
|
||||
ERR_PRINT_OFF;
|
||||
// This is invalid, as the number passed to `get_plural_message()` may not be negative.
|
||||
CHECK(vformat(translation->get_plural_message("There are %d apples", "", -1), -1) == "");
|
||||
ERR_PRINT_ON;
|
||||
CHECK(vformat(translation->get_plural_message("There are %d apples", "", 0), 0) == "Il y a 0 pomme");
|
||||
CHECK(vformat(translation->get_plural_message("There are %d apples", "", 1), 1) == "Il y a 1 pomme");
|
||||
CHECK(vformat(translation->get_plural_message("There are %d apples", "", 2), 2) == "Il y a 2 pommes");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("[Translation] Plural rules parsing") {
|
||||
ERR_PRINT_OFF;
|
||||
// This is invalid, as the number passed to `get_plural_message()` may not be negative.
|
||||
CHECK(vformat(translation->get_plural_message("There are %d apples", "", -1), -1) == "");
|
||||
{
|
||||
CHECK(PluralRules::parse("") == nullptr);
|
||||
|
||||
CHECK(PluralRules::parse("plurals=(n != 1);") == nullptr);
|
||||
CHECK(PluralRules::parse("nplurals; plurals=(n != 1);") == nullptr);
|
||||
CHECK(PluralRules::parse("nplurals=; plurals=(n != 1);") == nullptr);
|
||||
CHECK(PluralRules::parse("nplurals=0; plurals=(n != 1);") == nullptr);
|
||||
CHECK(PluralRules::parse("nplurals=-1; plurals=(n != 1);") == nullptr);
|
||||
|
||||
CHECK(PluralRules::parse("nplurals=2;") == nullptr);
|
||||
CHECK(PluralRules::parse("nplurals=2; plurals;") == nullptr);
|
||||
CHECK(PluralRules::parse("nplurals=2; plurals=;") == nullptr);
|
||||
}
|
||||
ERR_PRINT_ON;
|
||||
CHECK(vformat(translation->get_plural_message("There are %d apples", "", 0), 0) == "Il y a 0 pomme");
|
||||
CHECK(vformat(translation->get_plural_message("There are %d apples", "", 1), 1) == "Il y a 1 pomme");
|
||||
CHECK(vformat(translation->get_plural_message("There are %d apples", "", 2), 2) == "Il y a 2 pommes");
|
||||
|
||||
{
|
||||
PluralRules *pr = PluralRules::parse("nplurals=3; plural=(n==0 ? 0 : n==1 ? 1 : 2);");
|
||||
REQUIRE(pr != nullptr);
|
||||
|
||||
CHECK(pr->get_nplurals() == 3);
|
||||
CHECK(pr->get_plural() == "(n==0 ? 0 : n==1 ? 1 : 2)");
|
||||
|
||||
CHECK(pr->evaluate(0) == 0);
|
||||
CHECK(pr->evaluate(1) == 1);
|
||||
CHECK(pr->evaluate(2) == 2);
|
||||
CHECK(pr->evaluate(3) == 2);
|
||||
|
||||
memdelete(pr);
|
||||
}
|
||||
|
||||
{
|
||||
PluralRules *pr = PluralRules::parse("nplurals=1; plural=0;");
|
||||
REQUIRE(pr != nullptr);
|
||||
|
||||
CHECK(pr->get_nplurals() == 1);
|
||||
CHECK(pr->get_plural() == "0");
|
||||
|
||||
CHECK(pr->evaluate(0) == 0);
|
||||
CHECK(pr->evaluate(1) == 0);
|
||||
CHECK(pr->evaluate(2) == 0);
|
||||
CHECK(pr->evaluate(3) == 0);
|
||||
|
||||
memdelete(pr);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue