Merge pull request #70623 from timothyqiu/property-i18n

Separate property translation from editor translation, move sources to separate godot-editor-l10n repo
This commit is contained in:
Rémi Verschelde 2023-02-07 19:38:29 +01:00
commit a05670c617
No known key found for this signature in database
GPG Key ID: C3336907360768E1
173 changed files with 311381 additions and 4983518 deletions

View File

@ -768,6 +768,20 @@ StringName TranslationServer::doc_translate_plural(const StringName &p_message,
return p_message_plural;
}
void TranslationServer::set_property_translation(const Ref<Translation> &p_translation) {
property_translation = p_translation;
}
StringName TranslationServer::property_translate(const StringName &p_message) const {
if (property_translation.is_valid()) {
StringName r = property_translation->get_message(p_message);
if (r) {
return r;
}
}
return p_message;
}
bool TranslationServer::is_pseudolocalization_enabled() const {
return pseudolocalization_enabled;
}

View File

@ -78,6 +78,7 @@ class TranslationServer : public Object {
HashSet<Ref<Translation>> translations;
Ref<Translation> tool_translation;
Ref<Translation> doc_translation;
Ref<Translation> property_translation;
bool enabled = true;
@ -174,6 +175,8 @@ public:
void set_doc_translation(const Ref<Translation> &p_translation);
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;
void set_property_translation(const Ref<Translation> &p_translation);
StringName property_translate(const StringName &p_message) const;
void setup();

View File

@ -1,45 +0,0 @@
# Makefile providing various facilities to manage translations
TEMPLATE = classes.pot
POFILES = $(wildcard *.po)
LANGS = $(POFILES:%.po=%)
all: update merge
update:
@cd ../..; \
python3 doc/translations/extract.py \
--path doc/classes modules/*/doc_classes \
--output doc/translations/$(TEMPLATE)
merge:
@for po in $(POFILES); do \
echo -e "\nMerging $$po..."; \
msgmerge -w 79 -C $$po $$po $(TEMPLATE) > "$$po".new; \
mv -f "$$po".new $$po; \
msgattrib --output-file=$$po --no-obsolete $$po; \
done
check:
@for po in $(POFILES); do msgfmt -c $$po -o /dev/null; done
# Generate completion ratio from statistics string such as:
# 2775 translated messages, 272 fuzzy translations, 151 untranslated messages.
# First number can be 0, second and third numbers are only present if non-zero.
include-list:
@list=""; \
threshold=0.10; \
for po in $(POFILES); do \
res=`msgfmt --statistics $$po -o /dev/null 2>&1 | sed 's/[^0-9,]*//g'`; \
complete=`cut -d',' -f1 <<< $$res`; \
fuzzy_or_untranslated=`cut -d',' -f2 <<< $$res`; \
untranslated_maybe=`cut -d',' -f3 <<< $$res`; \
if [ -z "$$fuzzy_or_untranslated" ]; then fuzzy_or_untranslated=0; fi; \
if [ -z "$$untranslated_maybe" ]; then untranslated_maybe=0; fi; \
incomplete=`expr $$fuzzy_or_untranslated + $$untranslated_maybe`; \
if `awk "BEGIN {exit !($$complete / ($$complete + $$incomplete) > $$threshold)}"`; then \
lang=`basename $$po .po`; \
list+="$$lang,"; \
fi; \
done; \
echo $$list;

View File

@ -1 +0,0 @@
These `.po` and `.pot` files come from Weblate. Do not modify them manually.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,310 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import shutil
from collections import OrderedDict
EXTRACT_TAGS = ["description", "brief_description", "member", "constant", "theme_item", "link"]
HEADER = """\
# LANGUAGE translation of the Godot Engine class reference.
# Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md).
# Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
# This file is distributed under the same license as the Godot source code.
#
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Godot Engine class reference\\n"
"Report-Msgid-Bugs-To: https://github.com/godotengine/godot\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8-bit\\n"
"""
# Some strings used by make_rst.py are normally part of the editor translations,
# so we need to include them manually here for the online docs.
BASE_STRINGS = [
"Description",
"Tutorials",
"Properties",
"Constructors",
"Methods",
"Operators",
"Theme Properties",
"Signals",
"Enumerations",
"Constants",
"Property Descriptions",
"Constructor Descriptions",
"Method Descriptions",
"Operator Descriptions",
"Theme Property Descriptions",
"Inherits:",
"Inherited By:",
"(overrides %s)",
"Default",
"Setter",
"value",
"Getter",
"This method should typically be overridden by the user to have any effect.",
"This method has no side effects. It doesn't modify any of the instance's member variables.",
"This method accepts any number of arguments after the ones described here.",
"This method is used to construct a type.",
"This method doesn't need an instance to be called, so it can be called directly using the class name.",
"This method describes a valid operator to use with this type as left-hand operand.",
]
## <xml-line-number-hack from="https://stackoverflow.com/a/36430270/10846399">
import sys
sys.modules["_elementtree"] = None # type: ignore
import xml.etree.ElementTree as ET
## override the parser to get the line number
class LineNumberingParser(ET.XMLParser):
def _start(self, *args, **kwargs):
## Here we assume the default XML parser which is expat
## and copy its element position attributes into output Elements
element = super(self.__class__, self)._start(*args, **kwargs)
element._start_line_number = self.parser.CurrentLineNumber
element._start_column_number = self.parser.CurrentColumnNumber
element._start_byte_index = self.parser.CurrentByteIndex
return element
def _end(self, *args, **kwargs):
element = super(self.__class__, self)._end(*args, **kwargs)
element._end_line_number = self.parser.CurrentLineNumber
element._end_column_number = self.parser.CurrentColumnNumber
element._end_byte_index = self.parser.CurrentByteIndex
return element
## </xml-line-number-hack>
class Desc:
def __init__(self, line_no, msg, desc_list=None):
## line_no : the line number where the desc is
## msg : the description string
## desc_list : the DescList it belongs to
self.line_no = line_no
self.msg = msg
self.desc_list = desc_list
class DescList:
def __init__(self, doc, path):
## doc : root xml element of the document
## path : file path of the xml document
## list : list of Desc objects for this document
self.doc = doc
self.path = path
self.list = []
def print_error(error):
print("ERROR: {}".format(error))
## build classes with xml elements recursively
def _collect_classes_dir(path, classes):
if not os.path.isdir(path):
print_error("Invalid directory path: {}".format(path))
exit(1)
for _dir in map(lambda dir: os.path.join(path, dir), os.listdir(path)):
if os.path.isdir(_dir):
_collect_classes_dir(_dir, classes)
elif os.path.isfile(_dir):
if not _dir.endswith(".xml"):
# print("Got non-.xml file '{}', skipping.".format(path))
continue
_collect_classes_file(_dir, classes)
## opens a file and parse xml add to classes
def _collect_classes_file(path, classes):
if not os.path.isfile(path) or not path.endswith(".xml"):
print_error("Invalid xml file path: {}".format(path))
exit(1)
print("Collecting file: {}".format(os.path.basename(path)))
try:
tree = ET.parse(path, parser=LineNumberingParser())
except ET.ParseError as e:
print_error("Parse error reading file '{}': {}".format(path, e))
exit(1)
doc = tree.getroot()
if "name" in doc.attrib:
if "version" not in doc.attrib:
print_error("Version missing from 'doc', file: {}".format(path))
name = doc.attrib["name"]
if name in classes:
print_error("Duplicate class {} at path {}".format(name, path))
exit(1)
classes[name] = DescList(doc, path)
else:
print_error("Unknown XML file {}, skipping".format(path))
## regions are list of tuples with size 3 (start_index, end_index, indent)
## indication in string where the codeblock starts, ends, and it's indent
## if i inside the region returns the indent, else returns -1
def _get_xml_indent(i, regions):
for region in regions:
if region[0] < i < region[1]:
return region[2]
return -1
## find and build all regions of codeblock which we need later
def _make_codeblock_regions(desc, path=""):
code_block_end = False
code_block_index = 0
code_block_regions = []
while not code_block_end:
code_block_index = desc.find("[codeblock]", code_block_index)
if code_block_index < 0:
break
xml_indent = 0
while True:
## [codeblock] always have a trailing new line and some tabs
## those tabs are belongs to xml indentations not code indent
if desc[code_block_index + len("[codeblock]\n") + xml_indent] == "\t":
xml_indent += 1
else:
break
end_index = desc.find("[/codeblock]", code_block_index)
if end_index < 0:
print_error("Non terminating codeblock: {}".format(path))
exit(1)
code_block_regions.append((code_block_index, end_index, xml_indent))
code_block_index += 1
return code_block_regions
def _strip_and_split_desc(desc, code_block_regions):
desc_strip = "" ## a stripped desc msg
total_indent = 0 ## code indent = total indent - xml indent
for i in range(len(desc)):
c = desc[i]
if c == "\n":
c = "\\n"
if c == '"':
c = '\\"'
if c == "\\":
c = "\\\\" ## <element \> is invalid for msgmerge
if c == "\t":
xml_indent = _get_xml_indent(i, code_block_regions)
if xml_indent >= 0:
total_indent += 1
if xml_indent < total_indent:
c = "\\t"
else:
continue
else:
continue
desc_strip += c
if c == "\\n":
total_indent = 0
return desc_strip
## make catalog strings from xml elements
def _make_translation_catalog(classes):
unique_msgs = OrderedDict()
for class_name in classes:
desc_list = classes[class_name]
for elem in desc_list.doc.iter():
if elem.tag in EXTRACT_TAGS:
elem_text = elem.text
if elem.tag == "link":
elem_text = elem.attrib["title"] if "title" in elem.attrib else ""
if not elem_text or len(elem_text) == 0:
continue
line_no = elem._start_line_number if elem_text[0] != "\n" else elem._start_line_number + 1
desc_str = elem_text.strip()
code_block_regions = _make_codeblock_regions(desc_str, desc_list.path)
desc_msg = _strip_and_split_desc(desc_str, code_block_regions)
desc_obj = Desc(line_no, desc_msg, desc_list)
desc_list.list.append(desc_obj)
if desc_msg not in unique_msgs:
unique_msgs[desc_msg] = [desc_obj]
else:
unique_msgs[desc_msg].append(desc_obj)
return unique_msgs
## generate the catalog file
def _generate_translation_catalog_file(unique_msgs, output, location_line=False):
with open(output, "w", encoding="utf8") as f:
f.write(HEADER)
for msg in BASE_STRINGS:
f.write("#: doc/tools/make_rst.py\n")
f.write('msgid "{}"\n'.format(msg))
f.write('msgstr ""\n\n')
for msg in unique_msgs:
if len(msg) == 0 or msg in BASE_STRINGS:
continue
f.write("#:")
desc_list = unique_msgs[msg]
for desc in desc_list:
path = desc.desc_list.path.replace("\\", "/")
if path.startswith("./"):
path = path[2:]
if location_line: # Can be skipped as diffs on line numbers are spammy.
f.write(" {}:{}".format(path, desc.line_no))
else:
f.write(" {}".format(path))
f.write("\n")
f.write('msgid "{}"\n'.format(msg))
f.write('msgstr ""\n\n')
## TODO: what if 'nt'?
if os.name == "posix":
print("Wrapping template at 79 characters for compatibility with Weblate.")
os.system("msgmerge -w79 {0} {0} > {0}.wrap".format(output))
shutil.move("{}.wrap".format(output), output)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--path", "-p", nargs="+", default=".", help="The directory or directories containing XML files to collect."
)
parser.add_argument("--output", "-o", default="translation_catalog.pot", help="The path to the output file.")
args = parser.parse_args()
output = os.path.abspath(args.output)
if not os.path.isdir(os.path.dirname(output)) or not output.endswith(".pot"):
print_error("Invalid output path: {}".format(output))
exit(1)
classes = OrderedDict()
for path in args.path:
if not os.path.isdir(path):
print_error("Invalid working directory path: {}".format(path))
exit(1)
print("\nCurrent working dir: {}".format(path))
path_classes = OrderedDict() ## dictionary of key=class_name, value=DescList objects
_collect_classes_dir(path, path_classes)
classes.update(path_classes)
classes = OrderedDict(sorted(classes.items(), key=lambda kv: kv[0].lower()))
unique_msgs = _make_translation_catalog(classes)
_generate_translation_catalog_file(unique_msgs, output)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -75,10 +75,7 @@ if env.editor_build:
# Generated with `make include-list` for each resource.
# Editor translations
to_include = (
"ar,bg,ca,cs,de,el,eo,es_AR,es,fi,fr,gl,he,hu,id,it,ja,ko,lv,ms,nb,nl,pl,pt_BR,pt,ro,ru,sk,sv,th,tr,uk,vi,zh_CN,zh_TW"
).split(",")
tlist = [env.Dir("#editor/translations").abspath + "/" + f + ".po" for f in to_include]
tlist = glob.glob(env.Dir("#editor/translations/editor").abspath + "/*.po")
env.Depends("#editor/editor_translations.gen.h", tlist)
env.CommandNoCache(
"#editor/editor_translations.gen.h",
@ -86,9 +83,17 @@ if env.editor_build:
env.Run(editor_builders.make_editor_translations_header, "Generating editor translations header."),
)
# Property translations
tlist = glob.glob(env.Dir("#editor/translations/properties").abspath + "/*.po")
env.Depends("#editor/property_translations.gen.h", tlist)
env.CommandNoCache(
"#editor/property_translations.gen.h",
tlist,
env.Run(editor_builders.make_property_translations_header, "Generating property translations header."),
)
# Documentation translations
to_include = "de,es,fr,ja,zh_CN".split(",")
tlist = [env.Dir("#doc/translations").abspath + "/" + f + ".po" for f in to_include]
tlist = glob.glob(env.Dir("#doc/translations").abspath + "/*.po")
env.Depends("#editor/doc_translations.gen.h", tlist)
env.CommandNoCache(
"#editor/doc_translations.gen.h",

View File

@ -161,6 +161,10 @@ def make_editor_translations_header(target, source, env):
make_translations_header(target, source, env, "editor")
def make_property_translations_header(target, source, env):
make_translations_header(target, source, env, "property")
def make_doc_translations_header(target, source, env):
make_translations_header(target, source, env, "doc")

View File

@ -2974,11 +2974,11 @@ void EditorInspector::update_tree() {
// Only process group label if this is not the group or subgroup.
if ((i == 0 && component == group) || (i == 1 && component == subgroup)) {
if (section_name_style == EditorPropertyNameProcessor::STYLE_LOCALIZED) {
label = TTRGET(component);
label = EditorPropertyNameProcessor::get_singleton()->translate_group_name(component);
tooltip = component;
} else {
label = component;
tooltip = TTRGET(component);
tooltip = EditorPropertyNameProcessor::get_singleton()->translate_group_name(component);
}
} else {
label = EditorPropertyNameProcessor::get_singleton()->process_name(component, section_name_style);

View File

@ -30,6 +30,7 @@
#include "editor_property_name_processor.h"
#include "core/string/translation.h"
#include "editor_settings.h"
EditorPropertyNameProcessor *EditorPropertyNameProcessor::singleton = nullptr;
@ -92,18 +93,30 @@ String EditorPropertyNameProcessor::process_name(const String &p_name, Style p_s
} break;
case STYLE_LOCALIZED: {
return TTRGET(_capitalize_name(p_name));
const String capitalized = _capitalize_name(p_name);
if (TranslationServer::get_singleton()) {
return TranslationServer::get_singleton()->property_translate(capitalized);
}
return capitalized;
} break;
}
ERR_FAIL_V_MSG(p_name, "Unexpected property name style.");
}
String EditorPropertyNameProcessor::translate_group_name(const String &p_name) const {
if (TranslationServer::get_singleton()) {
return TranslationServer::get_singleton()->property_translate(p_name);
}
return p_name;
}
EditorPropertyNameProcessor::EditorPropertyNameProcessor() {
ERR_FAIL_COND(singleton != nullptr);
singleton = this;
// The following initialization is parsed in `editor/translations/extract.py` with a regex.
// The following initialization is parsed by the l10n extraction script with a regex.
// The map name and value definition format should be kept synced with the regex.
// https://github.com/godotengine/godot-editor-l10n/blob/main/scripts/common.py
capitalize_string_remaps["2d"] = "2D";
capitalize_string_remaps["3d"] = "3D";
capitalize_string_remaps["aa"] = "AA";
@ -263,7 +276,7 @@ EditorPropertyNameProcessor::EditorPropertyNameProcessor() {
capitalize_string_remaps["yz"] = "YZ";
// Articles, conjunctions, prepositions.
// The following initialization is parsed in `editor/translations/extract.py` with a regex.
// The following initialization is parsed in `editor/translations/scripts/common.py` with a regex.
// The word definition format should be kept synced with the regex.
stop_words = LocalVector<String>({
"a",

View File

@ -64,6 +64,9 @@ public:
// Turns property path segment into the given style.
String process_name(const String &p_name, Style p_style) const;
// Translate plain text group names.
String translate_group_name(const String &p_name) const;
EditorPropertyNameProcessor();
~EditorPropertyNameProcessor();
};

View File

@ -922,6 +922,7 @@ void EditorSettings::setup_language() {
}
// Load editor translation for configured/detected locale.
load_editor_translations(lang);
load_property_translations(lang);
// Load class reference translation.
load_doc_translations(lang);

View File

@ -35,6 +35,7 @@
#include "core/io/translation_loader_po.h"
#include "editor/doc_translations.gen.h"
#include "editor/editor_translations.gen.h"
#include "editor/property_translations.gen.h"
Vector<String> get_editor_locales() {
Vector<String> locales;
@ -101,3 +102,29 @@ void load_doc_translations(const String &p_locale) {
dtl++;
}
}
void load_property_translations(const String &p_locale) {
PropertyTranslationList *etl = _property_translations;
while (etl->data) {
if (etl->lang == p_locale) {
Vector<uint8_t> data;
data.resize(etl->uncomp_size);
int 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);
TranslationServer::get_singleton()->set_property_translation(tr);
break;
}
}
etl++;
}
}

View File

@ -37,5 +37,6 @@
Vector<String> get_editor_locales();
void load_editor_translations(const String &p_locale);
void load_doc_translations(const String &p_locale);
void load_property_translations(const String &p_locale);
#endif // EDITOR_TRANSLATION_H

View File

@ -1,42 +0,0 @@
# Makefile providing various facilities to manage translations
TEMPLATE = editor.pot
POFILES = $(wildcard *.po)
LANGS = $(POFILES:%.po=%)
all: update merge
update:
@cd ../..; python3 editor/translations/extract.py
merge:
@for po in $(POFILES); do \
echo -e "\nMerging $$po..."; \
msgmerge -w 79 -C $$po $$po $(TEMPLATE) > "$$po".new; \
mv -f "$$po".new $$po; \
msgattrib --output-file=$$po --no-obsolete $$po; \
done
check:
@for po in $(POFILES); do msgfmt -c $$po -o /dev/null; done
# Generate completion ratio from statistics string such as:
# 2775 translated messages, 272 fuzzy translations, 151 untranslated messages.
# First number can be 0, second and third numbers are only present if non-zero.
include-list:
@list=""; \
threshold=0.20; \
for po in $(POFILES); do \
res=`msgfmt --statistics $$po -o /dev/null 2>&1 | sed 's/[^0-9,]*//g'`; \
complete=`cut -d',' -f1 <<< $$res`; \
fuzzy_or_untranslated=`cut -d',' -f2 <<< $$res`; \
untranslated_maybe=`cut -d',' -f3 <<< $$res`; \
if [ -z "$$fuzzy_or_untranslated" ]; then fuzzy_or_untranslated=0; fi; \
if [ -z "$$untranslated_maybe" ]; then untranslated_maybe=0; fi; \
incomplete=`expr $$fuzzy_or_untranslated + $$untranslated_maybe`; \
if `awk "BEGIN {exit !($$complete / ($$complete + $$incomplete) > $$threshold)}"`; then \
lang=`basename $$po .po`; \
list+="$$lang,"; \
fi; \
done; \
echo $$list;

View File

@ -1,23 +0,0 @@
# How to contribute translations
Godot's translation work is coordinated on
[Hosted Weblate](https://hosted.weblate.org/projects/godot-engine/godot),
an open source web-based translation platform, where contributors can work
together on translations using various internationalization features.
Creating an account there is free, and you can also login directly with
your GitHub, BitBucket, Google or Facebook account.
To avoid merge conflicts when syncing translations from Weblate (currently
this is done manually), we ask all contributors to work there instead of
making pull requests on this repository.
Link if you missed it: https://hosted.weblate.org/projects/godot-engine/godot
## Adding new languages
If you want to translate for a language which is not featured yet on Weblate,
you can add it (when logged in) by clicking the "Start new translation"
button at the bottom of the page.
Alternatively, you can use this
[direct link](https://hosted.weblate.org/new-lang/godot-engine/godot/).

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More