diff --git a/build/compress.py b/build/compress.py index 538982669..dc723044d 100755 --- a/build/compress.py +++ b/build/compress.py @@ -27,240 +27,369 @@ # cycle since there is no need to rebuild or recompile, just reload. import json -import os.path +import os import re import subprocess import sys +from pathlib import Path +from typing import List - -if sys.version_info[0] < 3: - raise Exception("Must be using Python 3") +# Check Python version +if sys.version_info < (3, 6): + raise Exception("This script requires Python 3.6 or higher.") # Define a warning message for all the generated files. -WARNING = '// Automatically generated file. Do not edit!\n' - -blocklyMessageNames = [] -blocklyGamesMessageNames = [] - -def main(gameName): - print('Compressing %s' % gameName.title()) - if not os.path.exists('server/html/%s/generated' % gameName): - os.mkdir('server/html/%s/generated' % gameName) - generate_uncompressed(gameName) - generate_compressed(gameName) - filterMessages(gameName) - - # Extract the list of supported languages from boot.js. - # This is a bit fragile. - boot = open('server/html/common/boot.js', 'r') - js = ' '.join(boot.readlines()) - boot.close() - m = re.search('\\[\'BlocklyGamesLanguages\'\\] = (\\[[-,\'\\s\\w]+\\])', js) - if not m: - raise Exception("Can't find BlocklyGamesLanguages in boot.js") - langs = m.group(1) - langs = langs.replace("'", '"') - langs = json.loads(langs) - - for lang in langs: - language(gameName, lang) - print("") - - -def filterMessages(gameName): - global blocklyMessageNames, blocklyGamesMessageNames - # Identify all the Blockly messages used. - # Load the compiled game. - f = open('server/html/%s/generated/compressed.js' % gameName, 'r') - js = f.read() - f.close() - # Load any language file (they all should have the same keys). - msgs = getMessages('en') - for msg in msgs: - m = re.search('BlocklyMsg\\["([^"]+)"\\] = ', msg) - if m: - if (('"' + m.group(1) + '"') in js or - ('.' + m.group(1)) in js or - ('%{BKY_' + m.group(1) + '}') in js): - blocklyMessageNames.append(m.group(1)) - m = re.search('BlocklyGamesMsg\\["([^"]+)"\\] = ', msg) - if m: - if ('"' + m.group(1) + '"') in js or ('.' + m.group(1)) in js: - blocklyGamesMessageNames.append(m.group(1)) - print("Found %d Blockly messages." % len(blocklyMessageNames)) - blocklyMessageNames.sort() - print("Found %d Blockly Games messages." % len(blocklyGamesMessageNames)) - blocklyGamesMessageNames.sort() - - -def getMessages(lang): - # Read all messages for this language. - blocklyMsgFileName = 'server/html/generated/msg/%s.js' % lang - f = open(blocklyMsgFileName, 'r') - msgs = f.readlines() - f.close() - return msgs - - -def language(gameName, lang): - global blocklyMessageNames, blocklyGamesMessageNames - msgs = getMessages(lang) - # Only write out messages that are used (as detected in filterMessages). - bMsgs = [] - bgMsgs = [] - for msg in msgs: - m = re.search('BlocklyMsg\\["([^"]+)"\\] = (.*);\\s*', msg) - if m and m.group(1) in blocklyMessageNames: - # Blockly message names are all alphabetic, no need to quote. - bMsgs.append('%s:%s' % (m.group(1), m.group(2))) - m = re.search('BlocklyGamesMsg\\["([^"]+)"\\] = (.*);\\s*', msg) - if m and m.group(1) in blocklyGamesMessageNames: - # Blockly Games message names contain dots, quotes required. - bgMsgs.append('"%s":%s' % (m.group(1), m.group(2))) - - if not os.path.exists('server/html/%s/generated/msg' % gameName): - os.mkdir('server/html/%s/generated/msg' % gameName) - f = open('server/html/%s/generated/msg/%s.js' % (gameName, lang), 'w') - f.write(WARNING) - if bMsgs: - f.write('var BlocklyMsg={%s}\n' % ','.join(bMsgs)) - if bgMsgs: - f.write('var BlocklyGamesMsg={%s}\n' % ','.join(bgMsgs)) - f.close() - - -def generate_uncompressed(gameName): - cmd = ['third-party/closurebuilder/closurebuilder.py', - '--root=server/html/third-party/', - '--root=server/html/generated/', - '--root=server/html/src/', - '--exclude=', - '--namespace=%s' % gameName.replace('/', '.').title()] - directory = gameName - while directory: - subdir = 'server/html/%s/generated/' % directory - if os.path.isdir(subdir): - cmd.append('--root=%s' % subdir) - subdir = 'server/html/%s/src/' % directory - if os.path.isdir(subdir): - cmd.append('--root=%s' % subdir) - (directory, sep, fragment) = directory.rpartition(os.path.sep) - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) - except: - raise Exception("Failed to Popen: %s" % ' '.join(cmd)) - files = readStdout(proc) - - if gameName == 'pond/docs': - path = '../' - else: - path = '' - prefix = 'server/html/' - srcs = [] - for file in files: - file = file.strip() - if file[:len(prefix)] == prefix: - file = file[len(prefix):] - else: - raise Exception('"%s" is not in "%s".' % (file, prefix)) - srcs.append('"%s%s"' % (path, file)) - f = open('server/html/%s/generated/uncompressed.js' % gameName, 'w') - f.write("""%s +WARNING = '// Automatically generated file. Do not edit!\n' + +# Global variables to store message names (used across functions) +blocklyMessageNames: List[str] = [] +blocklyGamesMessageNames: List[str] = [] + +def main(gameName: str): + """ + Manages the compression and language file filtering process for the specified game. + """ + global blocklyMessageNames, blocklyGamesMessageNames + + print(f'🤖 Starting Build Process for {gameName.title()}...') + + # Define paths using Path objects for cross-OS compatibility + game_path = Path('server/html') / gameName + generated_path = game_path / 'generated' + + # Create necessary directories + try: + generated_path.mkdir(parents=True, exist_ok=True) + except OSError as e: + print(f"Error: Failed to create necessary directories: {e}") + sys.exit(1) + + # Perform compression first + generate_uncompressed(gameName, game_path, generated_path) + generate_compressed(gameName, game_path, generated_path) + + # Filter messages based on the compiled JS file + filterMessages(gameName, generated_path) + + # Extract the list of supported languages from boot.js. + langs = get_supported_languages() + + # Generate language files for each supported language + for lang in langs: + language(gameName, lang, generated_path) + + print(f"\n✅ Build Process for {gameName.title()} Completed.") + +def get_supported_languages() -> List[str]: + """ + Extracts the list of supported languages from common/boot.js. + """ + boot_file = Path('server/html/common/boot.js') + if not boot_file.exists(): + raise FileNotFoundError(f"Error: boot.js not found at {boot_file}") + + print(f"🌍 Retrieving Supported Languages...") + + try: + js = boot_file.read_text() + # Find the BlocklyGamesLanguages array definition + m = re.search(r'\[\'BlocklyGamesLanguages\'\] = (\[[-,\'\s\w]+\])', js) + + if not m: + raise Exception("Cannot find BlocklyGamesLanguages definition in boot.js") + + # Parse the JSON array + langs_str = m.group(1).replace("'", '"') + return json.loads(langs_str) + except Exception as e: + print(f"Error: Failed to extract languages: {e}") + sys.exit(1) + + +def filterMessages(gameName: str, generated_path: Path): + """ + Filters Blockly and Blockly Games messages based on usage within the compressed JS file. + Updates global blocklyMessageNames and blocklyGamesMessageNames lists. + """ + global blocklyMessageNames, blocklyGamesMessageNames + + compressed_file = generated_path / 'compressed.js' + if not compressed_file.exists(): + print(f"Error: compressed.js not found. Compilation may have failed: {compressed_file}") + return + + # Read the compressed code + js = compressed_file.read_text() + + # Get 'en' messages (assuming it contains all keys) + msgs = getMessages('en') + + # Regex patterns to find message definitions + blockly_re = re.compile(r'BlocklyMsg\["([^"]+)"\] = ') + bg_re = re.compile(r'BlocklyGamesMsg\["([^"]+)"\] = ') + + for msg in msgs: + # Check for Blockly messages + m = blockly_re.search(msg) + if m: + key = m.group(1) + # Check for different usage forms in the compiled code: quoted, dot notation, or %{BKY_...} + if (f'"{key}"' in js or + f'.{key}' in js or + f'%{{BKY_{key}}}' in js): + blocklyMessageNames.append(key) + + # Check for BlocklyGames messages + m = bg_re.search(msg) + if m: + key = m.group(1) + # Check for quoted or dot notation usage + if f'"{key}"' in js or f'.{key}' in js: + blocklyGamesMessageNames.append(key) + + # Deduplicate and sort the lists + blocklyMessageNames = sorted(list(set(blocklyMessageNames))) + blocklyGamesMessageNames = sorted(list(set(blocklyGamesMessageNames))) + + print(f"🏷️ Found {len(blocklyMessageNames)} used Blockly messages.") + print(f"🏷️ Found {len(blocklyGamesMessageNames)} used Blockly Games messages.") + + +def getMessages(lang: str) -> List[str]: + """ + Reads all messages for a specific language. + """ + msg_file = Path('server/html/generated/msg') / f'{lang}.js' + if not msg_file.exists(): + # This warning is expected for languages other than 'en' if msg compilation hasn't happened yet. + # But we only call this for 'en' in filterMessages, so it should exist. + print(f"Warning: Language file not found: {msg_file}") + return [] + + return msg_file.read_text().splitlines() + + +def language(gameName: str, lang: str, generated_path: Path): + """ + Generates the minimized JS message file for a specific language, + containing only the messages identified as used. + """ + global blocklyMessageNames, blocklyGamesMessageNames + + msgs = getMessages(lang) + if not msgs: + return # Skip if no messages found + + bMsgs = [] + bgMsgs = [] + + blockly_re = re.compile(r'BlocklyMsg\["([^"]+)"\] = (.*);\s*') + bg_re = re.compile(r'BlocklyGamesMsg\["([^"]+)"\] = (.*);\s*') + + for msg in msgs: + # Process Blockly Messages + m = blockly_re.search(msg) + if m and m.group(1) in blocklyMessageNames: + # Blockly message names are typically alphabetic, no need to quote keys. + bMsgs.append(f'{m.group(1)}:{m.group(2)}') + + # Process Blockly Games Messages + m = bg_re.search(msg) + if m and m.group(1) in blocklyGamesMessageNames: + # Blockly Games message names contain dots, quotes required for keys. + bgMsgs.append(f'"{m.group(1)}":{m.group(2)}') + + # Create the language file directory + msg_dir = generated_path / 'msg' + msg_dir.mkdir(parents=True, exist_ok=True) + + output_file = msg_dir / f'{lang}.js' + + # Write the minimized message objects + with output_file.open('w') as f: + f.write(WARNING) + if bMsgs: + f.write(f"var BlocklyMsg={{{','.join(bMsgs)}}}\n") + if bgMsgs: + f.write(f"var BlocklyGamesMsg={{{','.join(bgMsgs)}}}\n") + + print(f" - Message file created: {lang}.js") + + +def generate_uncompressed(gameName: str, game_path: Path, generated_path: Path): + """ + Uses Closure Builder to extract the dependency list and creates the uncompressed JS loader file. + """ + print("✨ Extracting Dependencies (uncompressed.js)...") + + cmd = [ + 'third-party/closurebuilder/closurebuilder.py', + '--root=server/html/third-party/', + '--root=server/html/generated/', + '--root=server/html/src/', + '--exclude=', + f'--namespace={gameName.replace("/", ".").title()}' + ] + + # Add source and generated directories from the game's path and its parent directories + current_dir = Path(gameName) + # Loop up the directory tree (e.g., from 'maze' to '.') + while current_dir != Path('.'): + subdir_generated = Path('server/html') / current_dir / 'generated/' + if subdir_generated.is_dir(): + cmd.append(f'--root={subdir_generated}') + subdir_src = Path('server/html') / current_dir / 'src/' + if subdir_src.is_dir(): + cmd.append(f'--root={subdir_src}') + + # Move to the parent directory + parent_dir = current_dir.parent + if parent_dir == current_dir: # Break if at the root + break + current_dir = parent_dir + + try: + # Run closurebuilder; output is a list of dependency files + proc = subprocess.run(cmd, capture_output=True, text=True, check=True) + files = proc.stdout.splitlines() + except subprocess.CalledProcessError as e: + print(f"Error: Closure Builder failed (uncompressed): {e.stderr}") + sys.exit(1) + except FileNotFoundError: + print(f"Error: closurebuilder.py not found. Check the path.") + sys.exit(1) + + # Process file paths to be relative to the HTML root + prefix = 'server/html/' + # Handle the special case where 'pond/docs' needs to access files in 'server/html/' via '../' + path_prefix = '../' if gameName == 'pond/docs' else '' + srcs = [] + + for file in files: + file = file.strip() + if file.startswith(prefix): + file = file[len(prefix):] + else: + raise Exception(f'"{file}" is not in the expected "{prefix}" directory.') + srcs.append(f'"{path_prefix}{file}"') + + output_file = generated_path / 'uncompressed.js' + + # Generate the JS loader script content + script_content = f"""{WARNING} window.CLOSURE_NO_DEPS = true; -(function() { - var srcs = [ - %s - ]; - function loadScript() { - var src = srcs.shift(); - if (src) { - var script = document.createElement('script'); - script.src = src; - script.type = 'text/javascript'; - script.onload = loadScript; - document.head.appendChild(script); - } - } - loadScript(); -})(); -""" % (WARNING, ',\n '.join(srcs))) - f.close() - print('Found %d dependencies.' % len(srcs)) - - -def generate_compressed(gameName): - cmd = [ - 'java', - '-jar', 'build/third-party-downloads/closure-compiler.jar', - '--generate_exports', - '--compilation_level', 'ADVANCED_OPTIMIZATIONS', - '--dependency_mode=PRUNE', - '--externs', 'externs/interpreter-externs.js', - '--externs', 'externs/prettify-externs.js', - '--externs', 'externs/soundJS-externs.js', - '--externs', 'externs/storage-externs.js', - '--externs', 'externs/svg-externs.js', - #'--language_in', 'STABLE', - '--language_out', 'ECMASCRIPT5', - '--entry_point=server/html/%s/src/main' % gameName, - "--js='server/html/third-party/base.js'", - "--js='server/html/third-party/blockly/**.js'", - "--js='server/html/src/*.js'", - '--warning_level', 'QUIET', - ] - directory = gameName - while directory: - cmd.append("--js='server/html/%s/src/*.js'" % directory) - (directory, sep, fragment) = directory.rpartition(os.path.sep) - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) - except: - print("Failed to Popen: %s" % cmd) - raise - script = readStdout(proc) - script = ''.join(script) - script = trim_licence(script) - print('Compressed to %d KB.' % (len(script) / 1024)) - - f = open('server/html/%s/generated/compressed.js' % gameName, 'w') - f.write(WARNING) - f.write(script) - f.close() - -def trim_licence(code): - """Strip out Google's and MIT's Apache licences. - - JS Compiler preserves dozens of Apache licences in the Blockly code. - Remove these if they belong to Google or MIT. - MIT's permission to do this is logged in Blockly issue 2412. - - Args: - code: Large blob of compiled source code. - - Returns: - Code with Google's and MIT's Apache licences trimmed. - """ - apache2 = re.compile("""/\\* - - (Copyright \\d+ (Google LLC|Massachusetts Institute of Technology)) -( All rights reserved. -)? SPDX-License-Identifier: Apache-2.0 -\\*/""") - return re.sub(apache2, '', code) - - -def readStdout(proc): - data = proc.stdout.readlines() - # Python 2 reads stdout as text. - # Python 3 reads stdout as bytes. - return list(map(lambda line: - type(line) == str and line or str(line, 'utf-8'), data)) - +(function() {{ + var srcs = [ + {',\n '.join(srcs)} + ]; + function loadScript() {{ + var src = srcs.shift(); + if (src) {{ + var script = document.createElement('script'); + script.src = src; + script.type = 'text/javascript'; + script.onload = loadScript; + document.head.appendChild(script); + }} + }} + loadScript(); +}})(); +""" + output_file.write_text(script_content) + print(f' - Found {len(srcs)} dependencies and created uncompressed.js.') + + +def generate_compressed(gameName: str, game_path: Path, generated_path: Path): + """ + Uses Closure Compiler to compress the JS code into a single file. + """ + print("🔥 Compressing JavaScript Code (compressed.js)...") + + # Define the main entry point + entry_point = Path('server/html') / gameName / 'src/main' + + cmd = [ + 'java', + '-jar', 'build/third-party-downloads/closure-compiler.jar', + '--generate_exports', + '--compilation_level', 'ADVANCED_OPTIMIZATIONS', + '--dependency_mode=PRUNE', + # External definitions (externs) for global symbols not known to the compiler + '--externs', 'externs/interpreter-externs.js', + '--externs', 'externs/prettify-externs.js', + '--externs', 'externs/soundJS-externs.js', + '--externs', 'externs/storage-externs.js', + '--externs', 'externs/svg-externs.js', + '--language_out', 'ECMASCRIPT5', + f'--entry_point={entry_point}', + # Base and common files + "--js='server/html/third-party/base.js'", + "--js='server/html/third-party/blockly/**.js'", + "--js='server/html/src/*.js'", + '--warning_level', 'QUIET', + ] + + # Add source files from the game's path and its parents + current_dir = Path(gameName) + while current_dir != Path('.'): + cmd.append(f"--js='server/html/{current_dir}/src/*.js'") + # Move to the parent directory + parent_dir = current_dir.parent + if parent_dir == current_dir: + break + current_dir = parent_dir + + try: + # Run Closure Compiler + proc = subprocess.run(cmd, capture_output=True, text=True, check=True) + script = proc.stdout + except subprocess.CalledProcessError as e: + print(f"Error: Closure Compiler failed (compressed): {e.stderr}") + sys.exit(1) + except FileNotFoundError: + print(f"Error: closure-compiler.jar not found. Check the path.") + sys.exit(1) + + # Strip out Apache 2.0 licenses from Google/MIT + script = trim_licence(script) + + # Write the output file + output_file = generated_path / 'compressed.js' + output_file.write_text(WARNING + script) + + print(f' - Compressed to {len(script) / 1024:.2f} KB.') + + +def trim_licence(code: str) -> str: + """ + Strips out Google's and MIT's Apache 2.0 licences from the compiled code. + + Args: + code: Large blob of compiled source code. + + Returns: + Code with Google's and MIT's Apache licences trimmed. + """ + # Regex pattern for the multi-line Apache 2.0 license header + apache2_re = re.compile( + r'/\*\s*\n' + r'(Copyright \d+ (Google LLC|Massachusetts Institute of Technology))' + r'( All rights reserved.\n)?' + r' SPDX-License-Identifier: Apache-2.0\n' + r'\*/', + re.DOTALL + ) + return apache2_re.sub('', code) + + +# The original readStdout function is no longer needed as subprocess.run(text=True) handles encoding. if __name__ == '__main__': - if len(sys.argv) == 2: - main(sys.argv[1]) - else: - print('Format: %s ' % sys.argv[0]) - sys.exit(2) + # Check for command line arguments + if len(sys.argv) == 2: + # Pass the game name to the main function + main(sys.argv[1]) + else: + print(f'Usage: {sys.argv[0]} ') + sys.exit(2) diff --git a/build/json_to_js.py b/build/json_to_js.py index 2131b4e5a..060eb8952 100755 --- a/build/json_to_js.py +++ b/build/json_to_js.py @@ -23,56 +23,77 @@ import os import re import sys +from pathlib import Path +from typing import Dict, List, Any - -if sys.version_info[0] < 3: - raise Exception("Must be using Python 3") +# Ensure compatibility with modern Python features +if sys.version_info < (3, 6): + raise Exception("This script requires Python 3.6 or higher.") def main(): - """Generate .js files defining Blockly Games messages.""" - - # Process command-line arguments. - parser = argparse.ArgumentParser(description='Convert JSON files to JS.') - parser.add_argument('--default_lang', - default='en', - help='Missing translations will come from this language.') - parser.add_argument('--blockly_msg_dir', - default=os.path.join('server', 'html', 'third-party', 'blockly', 'msg', 'json'), - help='Relative directory for Blockly\'s message .json files.') - parser.add_argument('--blocklygames_msg_dir', - default='json', - help='Relative directory for Blockly Games\' message .json files.') - parser.add_argument('--output_dir', - default=os.path.join('server', 'html', 'generated', 'msg'), - help='Relative directory for output .js files.') - args = parser.parse_args() - if not args.blockly_msg_dir.endswith(os.path.sep): - args.blockly_msg_dir += os.path.sep - if not args.blocklygames_msg_dir.endswith(os.path.sep): - args.blocklygames_msg_dir += os.path.sep - if not args.output_dir.endswith(os.path.sep): - args.output_dir += os.path.sep - os.makedirs(args.output_dir, exist_ok=True) - - blockly_constants_data = read_json_file(args.blockly_msg_dir, 'constants') - blockly_synonyms_data = read_json_file(args.blockly_msg_dir, 'synonyms') - blockly_default_data = read_json_file(args.blockly_msg_dir, args.default_lang) - bg_default_data = read_json_file(args.blocklygames_msg_dir, args.default_lang) - - language_files = glob.glob(os.path.join(args.blocklygames_msg_dir, '*.json')) - language_files.sort() - languages = [] - for language_file in language_files: - language = re.search(r'([\w-]+)\.json$', language_file)[1] - if language == 'qqq': - continue - if not os.path.isfile(os.path.join(args.blockly_msg_dir, language + '.json')): - # Need both the Blockly Games and Blockly message files. - continue - languages.append(language) - - output_file = codecs.open(os.path.join(args.output_dir, language + '.js'), 'w', 'utf-8') - output_file.write('''// This file was automatically generated. Do not modify. + """Generates .js files defining Blockly and Blockly Games messages.""" + print('Starting Message Compilation...') + + # Argument Parsing + parser = argparse.ArgumentParser(description='Convert JSON message files to JS message definitions.') + parser.add_argument('--default_lang', + default='en', + help='Missing translations will come from this default language.') + parser.add_argument('--blockly_msg_dir', + default=Path('server', 'html', 'third-party', 'blockly', 'msg', 'json'), + type=Path, + help='Relative directory for Blockly\'s message .json files.') + parser.add_argument('--blocklygames_msg_dir', + default=Path('json'), + type=Path, + help='Relative directory for Blockly Games\' message .json files.') + parser.add_argument('--output_dir', + default=Path('server', 'html', 'generated', 'msg'), + type=Path, + help='Relative directory for output .js files.') + args = parser.parse_args() + + # Create the output directory if it doesn't exist + args.output_dir.mkdir(parents=True, exist_ok=True) + + + try: + blockly_constants_data = read_json_file(args.blockly_msg_dir, 'constants') + blockly_synonyms_data = read_json_file(args.blockly_msg_dir, 'synonyms') + blockly_default_data = read_json_file(args.blockly_msg_dir, args.default_lang) + bg_default_data = read_json_file(args.blocklygames_msg_dir, args.default_lang) + except FileNotFoundError as e: + print(f"Error: Missing essential JSON file for default language or constants. Details: {e}") + sys.exit(1) + + + # Use glob with Path to find all language files in the Blockly Games directory. + language_files = sorted(args.blocklygames_msg_dir.glob('*.json')) + languages: List[str] = [] + + for language_file in language_files: + # Extract the language code from the filename + language = language_file.stem + if language == 'qqq': + continue + + # Check if the corresponding Blockly message file exists. + blockly_lang_path = args.blockly_msg_dir / f'{language}.json' + if not blockly_lang_path.is_file(): + # Must have both Blockly Games and Blockly message files to proceed. + print(f"Warning: Skipping language '{language}'. Blockly message file not found at {blockly_lang_path}.") + continue + + languages.append(language) + + # --- Generate Output JS File for Current Language + output_path = args.output_dir / f'{language}.js' + print(f"Processing messages for language: {language}") + + # Open the output file using codecs.open for explicit UTF-8 encoding + with codecs.open(output_path, 'w', 'utf-8') as output_file: + # Write the header + output_file.write('''// This file was automatically generated. Do not modify. 'use strict'; var BlocklyMsg = {}; @@ -80,64 +101,124 @@ def main(): ''') - # Write the Blockly messages. - blockly_language_data = read_json_file(args.blockly_msg_dir, language) - blockly_msg_dict = {} - for (name, default_message) in blockly_default_data.items(): - if name in blockly_language_data: - message_str = blockly_language_data[name] - comment = '' - else: - message_str = default_message - comment = ' // untranslated' - message_str = scrub_message(message_str) - blockly_msg_dict[name] = message_str - output_file.write('BlocklyMsg["%s"] = "%s";%s\n' % (name, message_str, comment)) - output_file.write('\n') - for (name, alias_name) in blockly_synonyms_data.items(): - blockly_msg_dict[name] = blockly_msg_dict[alias_name] - output_file.write('BlocklyMsg["%s"] = "%s";\n' % (name, blockly_msg_dict[alias_name])) + # 1. Write the Blockly messages. + write_blockly_messages(output_file, args.blockly_msg_dir, language, + blockly_default_data, blockly_synonyms_data, blockly_constants_data) + + # 2. Write the Blockly Games messages. + write_blockly_games_messages(output_file, args.blocklygames_msg_dir, language, bg_default_data) + + print(f'\n Message JS files generated for the following languages: {", ".join(languages)}') + + +def write_blockly_messages(output_file: Any, blockly_dir: Path, language: str, + default_data: Dict[str, str], synonyms_data: Dict[str, str], + constants_data: Dict[str, str]): + """Writes Blockly message definitions (translations, synonyms, constants) to the output file.""" + + try: + blockly_language_data = read_json_file(blockly_dir, language) + except FileNotFoundError: + # Should not happen due to the check in main(), but defensive programming. + print(f"Error: Blockly language file for '{language}' not found.") + return + + blockly_msg_dict: Dict[str, str] = {} + + # 1a. Write translated messages + for name, default_message in default_data.items(): + if name in blockly_language_data: + message_str = blockly_language_data[name] + comment = '' + else: + message_str = default_message + comment = ' // untranslated' + + message_str = scrub_message(message_str) + blockly_msg_dict[name] = message_str + output_file.write(f'BlocklyMsg["{name}"] = "{message_str}";{comment}\n') + output_file.write('\n') - for (name, message_str) in blockly_constants_data.items(): - message_str = scrub_message(message_str) - blockly_msg_dict[name] = message_str - output_file.write('BlocklyMsg["%s"] = "%s";\n' % (name, message_str)) + + # 1b. Write synonyms + for name, alias_name in synonyms_data.items(): + if alias_name in blockly_msg_dict: + alias_value = blockly_msg_dict[alias_name] + blockly_msg_dict[name] = alias_value + output_file.write(f'BlocklyMsg["{name}"] = BlocklyMsg["{alias_name}"]; // Synonym of {alias_name}\n') + else: + + + print(f"Warning: Synonym '{name}' references non-existent message '{alias_name}'.") output_file.write('\n') - - # Write the Blockly Games messages. - bg_language_data = read_json_file(args.blocklygames_msg_dir, language) - for (name, default_message) in bg_default_data.items(): - if name in bg_language_data: - message_str = bg_language_data[name] - comment = '' - else: - message_str = default_message - comment = ' // untranslated' - message_str = scrub_message(message_str) - output_file.write('BlocklyGamesMsg["%s"] = "%s";%s\n' % (name, message_str, comment)) - - output_file.close() - - print('Generated message js files for: ' + str(languages)) - - -def scrub_message(msg): - msg = msg.strip() - msg = msg.replace('\\', '\\\\') - msg = msg.replace('\n', '\\n') - msg = msg.replace('"', '\\"') - return msg + + # Write constants + for name, message_str in constants_data.items(): + message_str = scrub_message(message_str) + blockly_msg_dict[name] = message_str + output_file.write(f'BlocklyMsg["{name}"] = "{message_str}"; // Constant\n') + + output_file.write('\n') -def read_json_file(dir, isoCode): - json_file = codecs.open(os.path.join(dir, isoCode + '.json'), 'r', 'utf-8') - data = json.load(json_file) - json_file.close() - if '@metadata' in data: - del data['@metadata'] - return data +def write_blockly_games_messages(output_file: Any, bg_dir: Path, language: str, + default_data: Dict[str, str]): + """Writes Blockly Games message definitions (BlocklyGamesMsg["..."] = "...";) to the output file.""" + + try: + bg_language_data = read_json_file(bg_dir, language) + except FileNotFoundError: + # Should not happen due to the check in main. + print(f"Error: Blockly Games language file for '{language}' not found.") + return + + + for name, default_message in default_data.items(): + if name in bg_language_data: + message_str = bg_language_data[name] + comment = '' + else: + message_str = default_message + comment = ' // untranslated' + + message_str = scrub_message(message_str) + output_file.write(f'BlocklyGamesMsg["{name}"] = "{message_str}";{comment}\n') + + +def scrub_message(msg: str) -> str: + """ + Cleans up a message string for use in JavaScript, + escaping quotes, backslashes, and newlines. + """ + msg = msg.strip() + msg = msg.replace('\\', '\\\\') + msg = msg.replace('\n', '\\n') + msg = msg.replace('"', '\\"') + return msg + + +def read_json_file(directory: Path, isoCode: str) -> Dict[str, str]: + """Reads and parses a JSON message file, removing the @metadata tag.""" + file_path = directory / f'{isoCode}.json' + + if not file_path.is_file(): + raise FileNotFoundError(f"JSON file not found: {file_path}") + + # Use codecs.open for reliable UTF-8 reading + with codecs.open(file_path, 'r', 'utf-8') as json_file: + data = json.load(json_file) + + # Ensure all values are treated as strings + if not isinstance(data, dict): + raise ValueError(f"JSON file '{file_path}' does not contain a valid dictionary.") + + + if '@metadata' in data: + del data['@metadata'] + + return data if __name__ == '__main__': - main() + main() diff --git a/json/tr.json b/json/tr.json index f1b6e04c2..8b9ef8118 100644 --- a/json/tr.json +++ b/json/tr.json @@ -17,53 +17,54 @@ "Ramona.Stewart", "Sucsuzz", "Trockya", - "Velg" + "Velg", + "Alibehram11" ] - }, - "Games.name": "Blockly Oyunlar", + { + "Games.name": "Blockly Oyunları", "Games.puzzle": "Bulmaca", "Games.maze": "Labirent", "Games.bird": "Kuş", "Games.turtle": "Kaplumbağa", - "Games.movie": "Film", - "Games.music": "Müzik", + "Games.movie": "Film Yap", + "Games.music": "Müzik Yap", "Games.pondTutor": "Gölet Eğitimi", - "Games.pond": "Gölet", - "Games.linesOfCode1": "Bu seviyeyi 1 satır JavaScript ile çözdünüz:", - "Games.linesOfCode2": "Bu düzeyi %1 JavaScript satırı ile çözdünüz:", - "Games.nextLevel": "%1 seviyesine hazır mısınız?", - "Games.finalLevel": "Bir sonraki mücadeleye hazır mısın?", - "Games.submitTitle": "Başlık:", - "Games.linkTooltip": "Kaydedin ve bloklara bağlayın.", - "Games.runTooltip": "Yazdığınız programı çalıştırın.", - "Games.runProgram": "Programı Çalıştır", - "Games.resetTooltip": "Programı durdurun ve seviyeyi sıfırlayın.", + "Games.pond": "Gölet Oyunu", + "Games.linesOfCode1": "Bu seviyeyi sadece 1 satır JavaScript koduyla çözdün!", + "Games.linesOfCode2": "Bu seviyeyi %1 satır JavaScript koduyla çözdün!", + "Games.nextLevel": "%1 seviyesine geçmeye hazır mısın?", + "Games.finalLevel": "Sıradaki büyük meydan okumaya hazır mısın?", + "Games.submitTitle": "Başlık Gir:", + "Games.linkTooltip": "Kaydet ve bu bloklara bağlan.", + "Games.runTooltip": "Yazdığın programı çalıştır.", + "Games.runProgram": "Programı Başlat", + "Games.resetTooltip": "Programı durdur ve seviyeyi sıfırla.", "Games.resetProgram": "Sıfırla", - "Games.help": "Yardım", - "Games.catLogic": "Mantık", - "Games.catLoops": "Döngüler", + "Games.help": "Yardım Et", + "Games.catLogic": "Mantık İşlemleri", + "Games.catLoops": "Döngü Kullanımı", "Games.catMath": "Matematik", - "Games.catText": "Metin", + "Games.catText": "Metinler", "Games.catLists": "Listeler", - "Games.catColour": "Renk", + "Games.catColour": "Renkler", "Games.catVariables": "Değişkenler", - "Games.catProcedures": "İşlevler", - "Games.httpRequestError": "İstekle ilgili bir sorun oluştu.", - "Games.linkAlert": "Bloklarınızı bu bağlantıyla paylaşın:\n\n%1", - "Games.hashError": "Üzgünüz, '%1' kaydedilmiş bir programla uyuşmuyor.", - "Games.xmlError": "Kaydedilen dosya yüklenemedi. Belki de Blockly'nin farklı bir versiyonuyla oluşturuldu?", - "Games.submitted": "Bu program için teşekkürler! Eğer eğitimli maymun personelimiz bunu beğendiyse, birkaç gün içinde galeriye yayınlayacaklar.", + "Games.catProcedures": "Fonksiyonlar", + "Games.httpRequestError": "İsteği gerçekleştirirken bir sorun oluştu.", + "Games.linkAlert": "Bloklarını şu bağlantı ile paylaş:\n\n%1", + "Games.hashError": "Üzgünüm, '%1' kayıtlı bir programa uymuyor.", + "Games.xmlError": "Kaydedilmiş dosya yüklenemedi. Belki de Blockly'nin eski bir sürümüyle oluşturulmuştur?", + "Games.submitted": "Bu program için teşekkürler! Eğer eğitimli maymun ekibimiz beğenirse, birkaç gün içinde galeriye yayınlayacaklar.", "Games.listVariable": "liste", "Games.textVariable": "metin", - "Games.breakLink": "JavaScript'i düzenlemeye başladığınızda, düzenleme bloklarına geri dönemezsiniz. Bu tamam mı?", + "Games.breakLink": "JavaScript'i düzenlemeye başladığında, blok düzenlemeye geri dönemezsin. Tamam mı?", "Games.blocks": "Bloklar", "Games.congratulations": "Tebrikler!", - "Games.helpAbort": "Bu seviye son derece zordur. Atlamak ve bir sonraki oyuna geçmek ister misiniz? Daha sonra her zaman geri gelebilirsin.", - "Index.clear": "Tüm çözümleriniz silinsin mi?", - "Index.subTitle": "Yarının programcıları için oyunlar.", - "Index.moreInfo": "Eğitimciler için bilgi...", - "Index.startOver": "Baştan başlamak ister misiniz?", - "Index.clearData": "Verileri temizle", + "Games.helpAbort": "Bu seviye çok zorlu. Atlayıp sonraki oyuna geçmek ister misin? İstediğin zaman geri dönebilirsin.", + "Index.clear": "Tüm çözümlerin silinsin mi?", + "Index.subTitle": "Geleceğin programcıları için eğitici oyunlar.", + "Index.moreInfo": "Eğitimciler için ek bilgi...", + "Index.startOver": "Baştan mı başlamak istiyorsun?", + "Index.clearData": "Verileri Temizle", "Puzzle.animal1": "Ördek", "Puzzle.animal1Trait1": "Tüyler", "Puzzle.animal1Trait2": "Gaga", @@ -78,110 +79,110 @@ "Puzzle.animal3HelpUrl": "https://tr.wikipedia.org/wiki/Arı", "Puzzle.animal4": "Salyangoz", "Puzzle.animal4Trait1": "Kabuk", - "Puzzle.animal4Trait2": "Sümüksü madde", + "Puzzle.animal4Trait2": "Sümüksü Madde", "Puzzle.animal4HelpUrl": "https://tr.wikipedia.org/wiki/Salyangoz", - "Puzzle.picture": "resim:", - "Puzzle.legs": "bacaklar:", - "Puzzle.legsChoose": "seçim yapın...", - "Puzzle.traits": "özellikleri:", - "Puzzle.error0": "Muhteşem!\n%1 bloğun hepsi de doğru.", - "Puzzle.error1": "Neredeyse oluyordu! Bir blok yanlış.", - "Puzzle.error2": "%1 blok yanlış.", - "Puzzle.tryAgain": "Vurgulanan blok doğru değil.\nDenemeye devam et.", - "Puzzle.checkAnswers": "Yanıtları Kontrol Et", - "Puzzle.helpText": "Her hayvan için (yeşil) resmini ekleyin, bacak sayısını seçin ve özelliklerinin bir yığınını yapın.", - "Maze.moveForward": "ilerle", - "Maze.turnLeft": "sola dön", - "Maze.turnRight": "sağa dön", - "Maze.doCode": "yap", - "Maze.helpIfElse": "İf-else blokları bir şeyi yapar.", - "Maze.pathAhead": "eğer yol ileride ise", - "Maze.pathLeft": "sola giden yol", - "Maze.pathRight": "sağa giden yol", - "Maze.repeatUntil": "kadar tekrar edin", - "Maze.moveForwardTooltip": "Oynatıcıyı bir boşluk ileri götürür.", - "Maze.turnTooltip": "Oyuncuya 90 derece sola veya sağa çevirir.", - "Maze.ifTooltip": "Belirtilen yönde bir yol varsa, bazı eylemler yapın.", - "Maze.ifelseTooltip": "Belirtilen yönde bir yol varsa, ilk eylem bloğunu yapın. Aksi takdirde, ikinci eylem bloğunu yapın.", - "Maze.whileTooltip": "Bitiş noktasına ulaşılana kadar ekteki işlemleri tekrarlayın.", - "Maze.capacity0": "%0 bloğunuz kaldı.", - "Maze.capacity1": "%1 bloğunuz kaldı.", - "Maze.capacity2": "%2 bloğunuz kaldı.", - "Maze.runTooltip": "Oyuncunun blokların söylediklerini yapmasını sağlar.", - "Maze.resetTooltip": "Oynatıcıyı labirentin başlangıcına geri koyun.", - "Maze.helpStack": "Hedefe ulaşmama yardımcı olmak için birkaç 'ileri' bloğunu bir araya getir.", - "Maze.helpOneTopBlock": "Bu seviyede, beyaz çalışma alanındaki tüm blokları bir araya getirmeniz gerekir.", - "Maze.helpRun": "Ne olduğunu görmek için programınızı çalıştırın.", - "Maze.helpReset": "Programınız labirenti çözmedi. 'Sıfırla'ya basın ve tekrar deneyin.", - "Maze.helpRepeat": "Yalnızca iki blok kullanarak bu yolun sonuna ulaşın. Bir bloğu bir kereden fazla çalıştırmak için 'tekrarla'yı kullanın.", - "Maze.helpCapacity": "Bu seviye için tüm blokları kullandınız. Yeni bir blok oluşturmak için önce mevcut bir bloğu silmeniz gerekir.", - "Maze.helpRepeatMany": "'tekrar' bloğuna birden fazla blok takabilirsiniz.", - "Maze.helpSkins": "Bu menüden favori oynatıcınızı seçin.", - "Maze.helpIf": "'if' bloğu yalnızca koşul doğruysa bir şeyler yapar. Sola giden bir yol varsa sola dönmeyi deneyin.", - "Maze.helpMenu": "Durumunu değiştirmek için 'if' bloğunda %1 tıklayın.", - "Maze.helpWallFollow": "Bu karmaşık labirenti çözebilir misin? Sol duvarı takip etmeyi deneyin. Yalnızca gelişmiş programcılar!", - "Bird.noWorm": "solucanı yok", - "Bird.heading": "başlığı", - "Bird.noWormTooltip": "Kuş solucanı almadığı durum.", - "Bird.headingTooltip": "Verilen açının yönünde hareket edin: 0 sağa, 90 düz yukarı vb.", - "Bird.positionTooltip": "x ve y, kuşun konumunu işaretler. X = 0 olduğunda kuş sol kenara yakındır, x = 100 olduğunda sağ kenara yakındır. Y = 0 olduğunda kuş en altta, y = 100 olduğunda en üsttedir.", - "Bird.helpHeading": "Kuşun solucanını ve yuvasına inmesini sağlamak için yön açısını değiştirin.", - "Bird.helpHasWorm": "Solucanınız varsa bir başlığa veya solucanınız yoksa farklı bir başlığa gitmek için bu bloğu kullanın.", - "Bird.helpX": "'x' geçerli yatay konumunuzdur. 'x' sayıdan küçükse bir başlıkta, aksi takdirde farklı bir başlıkta gitmek için bu bloğu kullanın.", - "Bird.helpElse": "'if' bloğunu değiştirmek için simgeyi tıklayın.", - "Bird.helpElseIf": "Bu seviye hem 'else if' hem de 'else' bloğuna ihtiyaç duyar.", - "Bird.helpAnd": "'and' bloğu yalnızca her iki girişi de doğruysa doğrudur.", - "Bird.helpMutator": "'else' bloğunu 'if' bloğuna sürükleyin.", - "Turtle.moveTooltip": "Kaplumbağayı belirtilen miktarda ileri veya geri hareket ettirir.", - "Turtle.moveForward": "şu kadar ileri git", - "Turtle.moveBackward": "şu kadar geri git", + "Puzzle.picture": "Resim:", + "Puzzle.legs": "Bacak Sayısı:", + "Puzzle.legsChoose": "Seçim Yap...", + "Puzzle.traits": "Özellikleri:", + "Puzzle.error0": "Harika!\n%1 bloğun hepsi de doğru yerleştirilmiş.", + "Puzzle.error1": "Neredeyse başardın! Yalnızca bir blok yanlış.", + "Puzzle.error2": "%1 blok yanlış görünüyor.", + "Puzzle.tryAgain": "Vurgulanan blok hatalı. Tekrar denemeye devam et.", + "Puzzle.checkAnswers": "Cevapları Kontrol Et", + "Puzzle.helpText": "Her hayvan için (yeşil) resmini ekle, bacak sayısını seç ve özelliklerini yığın halinde birleştir.", + "Maze.moveForward": "İlerle", + "Maze.turnLeft": "Sola Dön", + "Maze.turnRight": "Sağa Dön", + "Maze.doCode": "Yap", + "Maze.helpIfElse": "Eğer-Değilse blokları, koşula göre farklı şeyler yapmanı sağlar.", + "Maze.pathAhead": "Eğer İleride Yol Varsa", + "Maze.pathLeft": "Eğer Sola Yol Varsa", + "Maze.pathRight": "Eğer Sağa Yol Varsa", + "Maze.repeatUntil": "Şu Ana Kadar Tekrarla", + "Maze.moveForwardTooltip": "Oynatıcıyı bir adım ileri götürür.", + "Maze.turnTooltip": "Oyuncuyu 90 derece sola veya sağa döndürür.", + "Maze.ifTooltip": "Belirtilen yönde bir yol varsa, o eylemi gerçekleştir.", + "Maze.ifelseTooltip": "Belirtilen yönde bir yol varsa ilk eylemi, aksi takdirde ikinci eylemi gerçekleştir.", + "Maze.whileTooltip": "Bitiş noktasına ulaşılana kadar eklenen işlemleri tekrar et.", + "Maze.capacity0": "Kullanabileceğin %0 blok kaldı.", + "Maze.capacity1": "Kullanabileceğin %1 blok kaldı.", + "Maze.capacity2": "Kullanabileceğin %2 blok kaldı.", + "Maze.runTooltip": "Oyuncunun bloklarda yazılanları yapmasını sağlar.", + "Maze.resetTooltip": "Oynatıcıyı labirentin başlangıcına geri döndür.", + "Maze.helpStack": "Hedefe ulaşmak için birkaç 'ilerle' bloğunu birleştir.", + "Maze.helpOneTopBlock": "Bu seviyede, çalışma alanındaki tüm blokları tek bir yığında birleştirmen gerekiyor.", + "Maze.helpRun": "Ne olduğunu görmek için programını çalıştır.", + "Maze.helpReset": "Programın labirenti çözmedi. 'Sıfırla'ya bas ve tekrar dene.", + "Maze.helpRepeat": "Yalnızca iki blok kullanarak yolun sonuna ulaş. Bir bloğu birden fazla çalıştırmak için 'tekrarla'yı kullan.", + "Maze.helpCapacity": "Bu seviye için tüm blokları kullandın. Yeni bir blok eklemek için mevcut bir bloğu silmelisin.", + "Maze.helpRepeatMany": "'Tekrarla' bloğuna birden fazla blok takabilirsin.", + "Maze.helpSkins": "Bu menüden favori oynatıcını seçebilirsin.", + "Maze.helpIf": "'Eğer' bloğu yalnızca koşul doğruysa bir şeyler yapar. Sola yol varsa sola dönmeyi dene.", + "Maze.helpMenu": "Durumunu değiştirmek için 'eğer' bloğundaki %1 simgesine tıkla.", + "Maze.helpWallFollow": "Bu karmaşık labirenti çözebilir misin? Sol duvarı takip etmeyi dene. Sadece ileri düzey programcılar için!", + "Bird.noWorm": "Solucan Yok", + "Bird.heading": "Yön Açısı", + "Bird.noWormTooltip": "Kuşun solucanı olmadığı durumu belirtir.", + "Bird.headingTooltip": "Belirtilen açı yönünde hareket et: 0 sağ, 90 yukarı, vb.", + "Bird.positionTooltip": "x ve y, kuşun konumunu gösterir. X=0 sol kenar, X=100 sağ kenar. Y=0 alt, Y=100 üst kenar.", + "Bird.helpHeading": "Kuşun solucanı almasını ve yuvaya inmesini sağlamak için yön açısını değiştir.", + "Bird.helpHasWorm": "Solucan varsa bir yöne, yoksa farklı bir yöne gitmek için bu bloğu kullan.", + "Bird.helpX": "'X', anlık yatay konumun. 'X' bir sayıdan küçükse bir yöne, aksi takdirde farklı bir yöne gitmek için bu bloğu kullan.", + "Bird.helpElse": "'Eğer' bloğunu değiştirmek için simgeye tıkla.", + "Bird.helpElseIf": "Bu seviye hem 'değilse eğer' hem de 'değilse' bloklarına ihtiyaç duyar.", + "Bird.helpAnd": "'Ve' bloğu, ancak her iki girişi de doğruysa doğru sonuç verir.", + "Bird.helpMutator": "'Değilse' bloğunu 'eğer' bloğunun içine sürükle.", + "Turtle.moveTooltip": "Kaplumbağayı belirtilen miktar kadar ileri veya geri hareket ettirir.", + "Turtle.moveForward": "Şu kadar ileri git", + "Turtle.moveBackward": "Şu kadar geri git", "Turtle.turnTooltip": "Kaplumbağayı belirtilen derece kadar sola veya sağa çevirir.", - "Turtle.turnRight": "şu kadar sağa dön", - "Turtle.turnLeft": "şu kadar sola dön", + "Turtle.turnRight": "Şu kadar sağa dön", + "Turtle.turnLeft": "Şu kadar sola dön", "Turtle.widthTooltip": "Kalemin genişliğini değiştirir.", - "Turtle.setWidth": "genişliği ayarla", + "Turtle.setWidth": "Genişliği Ayarla", "Turtle.colourTooltip": "Kalemin rengini değiştirir.", - "Turtle.setColour": "rengi ayarla", + "Turtle.setColour": "Rengi Ayarla", "Turtle.penTooltip": "Çizimi durdurmak veya başlatmak için kalemi kaldırır veya indirir.", - "Turtle.penUp": "kalemi kaldır", - "Turtle.penDown": "kalemi indir", - "Turtle.turtleVisibilityTooltip": "Kaplumbağayı (daire ve ok) görünür veya görünmez yapar.", - "Turtle.hideTurtle": "kaplumbağayı gizle", - "Turtle.showTurtle": "kaplumbağayı göster", + "Turtle.penUp": "Kalemi Kaldır", + "Turtle.penDown": "Kalemi İndir", + "Turtle.turtleVisibilityTooltip": "Kaplumbağayı (daire ve ok) görünür veya gizli yapar.", + "Turtle.hideTurtle": "Kaplumbağayı Gizle", + "Turtle.showTurtle": "Kaplumbağayı Göster", "Turtle.printHelpUrl": "https://tr.wikipedia.org/wiki/Matbaacılık", - "Turtle.printTooltip": "Metni, kaplumbağa yönünde bulunduğu yere çizer.", - "Turtle.print": "yazdır", + "Turtle.printTooltip": "Metni, kaplumbağanın bulunduğu yönde çizer.", + "Turtle.print": "Yazdır", "Turtle.fontHelpUrl": "https://tr.wikipedia.org/wiki/Yazı_tipi", "Turtle.fontTooltip": "Yazdırma bloğu tarafından kullanılan yazı tipini ayarlar.", - "Turtle.font": "yazı tipi", - "Turtle.fontSize": "yazı tipi boyutu", - "Turtle.fontNormal": "normal", - "Turtle.fontBold": "kalın", - "Turtle.fontItalic": "eğik", - "Turtle.submitDisabled": "Programınızı durana kadar çalıştırın. Ardından çiziminizi galeriye gönderebilirsiniz.", - "Turtle.galleryTooltip": "Çizimler galerisini açın.", - "Turtle.galleryMsg": "Galeriye Bakın", - "Turtle.submitTooltip": "Çiziminizi galeriye gönderin.", + "Turtle.font": "Yazı Tipi", + "Turtle.fontSize": "Yazı Tipi Boyutu", + "Turtle.fontNormal": "Normal", + "Turtle.fontBold": "Kalın", + "Turtle.fontItalic": "Eğik", + "Turtle.submitDisabled": "Programını durana kadar çalıştır. Sonra çizimini galeriye gönderebilirsin.", + "Turtle.galleryTooltip": "Çizimler galerisini aç.", + "Turtle.galleryMsg": "Galeriye Göz At", + "Turtle.submitTooltip": "Çizimini galeriye gönder.", "Turtle.submitMsg": "Galeriye Gönder", - "Turtle.helpUseLoop": "Çözümünüz işe yarıyor, ancak daha iyisini yapabilirsiniz.", - "Turtle.helpUseLoop3": "Şekli yalnızca üç blokla çizin.", - "Turtle.helpUseLoop4": "Yıldızı yalnızca dört blokla çizin.", - "Turtle.helpText1": "Kare çizen bir program oluşturun.", - "Turtle.helpText2": "Kare yerine beşgen çizmek için programınızı değiştirin.", - "Turtle.helpText3a": "Rengi değiştirmenize izin veren yeni bir blok var:", - "Turtle.helpText3b": "Sarı bir yıldız çizin.", - "Turtle.helpText4a": "Hareket ettiğinizde kaleminizi kağıttan kaldırmanıza izin veren yeni bir blok var:", - "Turtle.helpText4b": "Küçük bir sarı yıldız çizin, ardından üzerine bir çizgi çizin.", - "Turtle.helpText5": "Bir yıldız yerine, bir karede düzenlenmiş dört yıldız çizebilir misiniz?", - "Turtle.helpText6": "Üç sarı yıldız ve bir beyaz çizgi çizin.", - "Turtle.helpText7": "Yıldızları çizin, ardından dört beyaz çizgi çizin.", - "Turtle.helpText8": "360 beyaz çizgi çizmek dolunay gibi görünecektir.", - "Turtle.helpText9": "Ayın hilal haline gelmesi için siyah bir daire ekleyebilir misiniz?", - "Turtle.helpText10": "İstediğiniz her şeyi çizin. Keşfedebileceğiniz çok sayıda yeni blok var. İyi eğlenceler!", - "Turtle.helpText10Reddit": "Başkalarının çizdiklerini görmek için 'Galeriye Bakın' düğmesini kullanın. İlginç bir şey çizerseniz, yayınlamak için 'Galeriye Gönder' düğmesini kullanın.", - "Turtle.helpToolbox": "Blokları görmek için bir kategori seç.Blokları görmek için bir kategori seçin.", - "Movie.x": "x", - "Movie.y": "y", + "Turtle.helpUseLoop": "Çözümün çalışıyor, ama daha iyisini yapabilirsin.", + "Turtle.helpUseLoop3": "Şekli yalnızca üç blok kullanarak çiz.", + "Turtle.helpUseLoop4": "Yıldızı sadece dört blokla çiz.", + "Turtle.helpText1": "Bir kare çizen bir program oluştur.", + "Turtle.helpText2": "Kare yerine beşgen çizmek için programını değiştir.", + "Turtle.helpText3a": "Rengi değiştirebileceğin yeni bir blok var:", + "Turtle.helpText3b": "Sarı bir yıldız çiz.", + "Turtle.helpText4a": "Hareket ederken kalemini kağıttan kaldırmana yarayan yeni bir blok var:", + "Turtle.helpText4b": "Küçük sarı bir yıldız çiz, sonra üzerine bir çizgi ekle.", + "Turtle.helpText5": "Bir yıldız yerine, kare şeklinde düzenlenmiş dört yıldız çizebilir misin?", + "Turtle.helpText6": "Üç sarı yıldız ve bir beyaz çizgi çiz.", + "Turtle.helpText7": "Yıldızları çiz, ardından dört beyaz çizgi çiz.", + "Turtle.helpText8": "360 beyaz çizgi çizmek, dolunay gibi görünecektir.", + "Turtle.helpText9": "Ayın hilal olması için siyah bir daire ekleyebilir misin?", + "Turtle.helpText10": "İstediğin her şeyi çiz! Keşfedebileceğin bir sürü yeni blok var. İyi eğlenceler!", + "Turtle.helpText10Reddit": "Başkalarının çizdiklerini görmek için 'Galeriye Göz At' düğmesini kullan. İlginç bir şey çizersen, yayınlamak için 'Galeriye Gönder' düğmesini kullan.", + "Turtle.helpToolbox": "Blokları görmek için bir kategori seç.", + "Movie.x": "x konumu", + "Movie.y": "y konumu", "Movie.x1": "başlangıç x", "Movie.y1": "başlangıç y", "Movie.x2": "bitiş x", @@ -189,95 +190,95 @@ "Movie.radius": "yarıçap", "Movie.width": "genişlik", "Movie.height": "yükseklik", - "Movie.circleTooltip": "Belirtilen yere ve belirtilen yarıçapa sahip bir daire çizer.", - "Movie.circleDraw": "daire", - "Movie.rectTooltip": "Belirtilen konuma ve belirtilen genişlik ve yüksekliğe sahip bir dikdörtgen çizer.", - "Movie.rectDraw": "dikdörtgen", - "Movie.lineTooltip": "Belirtilen genişlikte bir noktadan diğerine bir çizgi çizer.", - "Movie.lineDraw": "satır", - "Movie.timeTooltip": "Animasyondaki geçerli saati döndürür (0-100).", + "Movie.circleTooltip": "Belirtilen konumda ve yarıçapta bir daire çizer.", + "Movie.circleDraw": "Daire Çiz", + "Movie.rectTooltip": "Belirtilen konumda, genişlikte ve yükseklikte bir dikdörtgen çizer.", + "Movie.rectDraw": "Dikdörtgen Çiz", + "Movie.lineTooltip": "Belirtilen genişlikte bir noktadan diğerine çizgi çizer.", + "Movie.lineDraw": "Çizgi Çiz", + "Movie.timeTooltip": "Animasyondaki geçerli süreyi döndürür (0 ile 100 arası).", "Movie.colourTooltip": "Kalemin rengini değiştirir.", - "Movie.setColour": "rengi olarak ayarla", - "Movie.submitDisabled": "Filminiz hareket etmiyor. İlginç bir şey yapmak için blokları kullanın. Ardından filminizi galeriye gönderebilirsiniz.", - "Movie.galleryTooltip": "Film galerisini açın.", - "Movie.galleryMsg": "Galeriye Bakın", - "Movie.submitTooltip": "Filminizi galeriye gönderin.", - "Movie.submitMsg": "Galeriye Gönderin", - "Movie.helpLayer": "Arka plan dairesini programınızın üstüne taşıyın. Sonra kişinin arkasında görünecektir.", - "Movie.helpText1": "Bu kişiyi çizmek için basit şekiller kullanın.", - "Movie.helpText2a": "Bu seviye bir film. Kişinin kolunun ekran boyunca hareket etmesini istiyorsunuz. Bir önizleme görmek için oynat düğmesine basın.", - "Movie.helpText2b": "Film oynatılırken, 'zaman' bloğunun değeri 0'dan 100'e kadar sayar. Kolun 'y' konumunun 0'dan başlamasını ve 100'e gitmesini istediğiniz için bu kolay olmalıdır.", - "Movie.helpText3": "'Zaman' bloğu 0'dan 100'e kadar sayılır. Ama şimdi diğer kolun 'y' pozisyonunun 100'de başlamasını ve 0'a gitmesini istiyorsunuz. Yönü çeviren basit bir matematiksel formül bulabilir misiniz?", - "Movie.helpText4": "Bacakları çapraz yapmak için önceki seviyede öğrendiklerini kullanın.", - "Movie.helpText5": "Kol için matematiksel formül karmaşıktır. İşte cevap:", - "Movie.helpText6": "Kişiye birkaç el verin.", - "Movie.helpText7": "Filmin ilk yarısına küçük bir kafa çizmek için 'if' bloğunu kullanın. Ardından filmin ikinci yarısı için büyük bir kafa çizin.", - "Movie.helpText8": "Bacakları filmin ortasında ters yönde hareket ettirin.", - "Movie.helpText9": "Kişinin arkasına genişleyen bir daire çizin.", - "Movie.helpText10": "İstediğiniz her şeyin bir filmini yapın. Keşfedebileceğiniz çok sayıda yeni blok var. İyi eğlenceler!", - "Movie.helpText10Reddit": "Diğer kişilerin çektiği filmleri görmek için 'Galeriye Bakın' düğmesini kullanın. İlginç bir film yaparsanız, yayınlamak için 'Galeriye Gönder' düğmesini kullanın.", - "Music.playNoteTooltip": "Belirtilen süre ve adımda bir nota çalar.", - "Music.playNote": "%1 sürede %2 oynat", - "Music.restTooltip": "Belirtilen süre kadar bekler.", - "Music.restWholeTooltip": "Bütün bir notu bekler.", - "Music.rest": "kalan süre: %1", - "Music.setInstrumentTooltip": "Sonraki müzik notalarını çalarken belirtilen enstrümana geçer.", - "Music.setInstrument": "enstrümanı %1 olarak ayarla", - "Music.startTooltip": "'Programı Çalıştır' düğmesine tıklandığında içerideki blokları yürütür.", - "Music.start": "%1 simgesine tıkladığınızda", - "Music.pitchTooltip": "Bir not (C4 7'dir).", - "Music.firstPart": "ilk kısım", - "Music.piano": "piyano", - "Music.trumpet": "trompet", - "Music.banjo": "banço", - "Music.violin": "keman", - "Music.guitar": "gitar", - "Music.flute": "flüt", - "Music.drum": "davul", - "Music.choir": "koro", - "Music.submitDisabled": "Programınızı durana kadar çalıştırın. Ardından müziğinizi galeriye gönderebilirsiniz.", - "Music.galleryTooltip": "Müzik galerisini açın.", - "Music.galleryMsg": "Galeriyi Bakın", - "Music.submitTooltip": "Müziğinizi galeriye gönderin.", - "Music.submitMsg": "Galeriye Gönderin", - "Music.helpUseFunctions": "Çözümünüz işe yarıyor, ancak daha iyisini yapabilirsiniz. Tekrarlanan kod miktarını azaltmak için fonksiyonları kullanın.", - "Music.helpUseInstruments": "Her başlangıç bloğunda farklı bir enstrüman kullanırsanız müzik daha iyi ses çıkarır.", - "Music.helpText1": "'Frère Jacques'in ilk dört notunu yazın.", - "Music.helpText2a": "Bir 'function' blokları bir arada gruplandırmanıza ve sonra birden fazla çalıştırmanıza izin verir.", - "Music.helpText2b": "'Frère Jacques'in ilk dört notasını çalmak için bir işlev oluşturun. Bu işlevi iki kez çalıştırın. Yeni not blokları eklemeyin.", - "Music.helpText3": "'Frère Jacques'in sonraki kısmı için ikinci bir fonksiyon oluşturun. Son not daha uzundur.", - "Music.helpText4": "'Frère Jacques'in sonraki kısmı için üçüncü bir fonksiyon oluşturun. İlk dört not daha kısadır.", - "Music.helpText5": "'Frère Jacques' şarkısının tamamını tamamlayın.", - "Music.helpText6a": "Bu yeni blok, başka bir enstrümana geçmenizi sağlar.", - "Music.helpText6b": "Melodinizi bir keman ile çalın.", - "Music.helpText7a": "Bu yeni blok sessiz bir gecikme ekler.", - "Music.helpText7b": "İki gecikme bloğuna sahip ikinci bir başlangıç bloğu oluşturun, ardından 'Frère Jacques' oynayın.", - "Music.helpText8": "Her başlangıç bloğunda iki kez 'Frère Jacques' oynanmalıdır.", - "Music.helpText9": "Her biri iki kez 'Frère Jacques' oynayan dört başlangıç bloğu oluşturun. Doğru sayıda gecikme bloğu ekleyin.", - "Music.helpText10": "İstediğiniz her şeyi oluşturun. Keşfedebileceğiniz çok sayıda yeni blok var. İyi eğlenceler!", - "Music.helpText10Reddit": "Başkalarının yazdıklarını görmek için 'Galeriye Bakın' düğmesini kullanın. İlginç bir şey oluşturursanız, yayınlamak için 'Galeriye Gönder' düğmesini kullanın.", - "Pond.scanTooltip": "Düşmanları tarayın. Bir yön belirtin (0-360). Bu yöndeki en yakın düşmana olan mesafeyi verir. Hiç düşman bulunmazsa Sonsuzluğu döndürür.", - "Pond.cannonTooltip": "Topu ateşle. Bir yön (0-360) ve bir aralık (0-70) belirleyin.", - "Pond.swimTooltip": "İleriye doğru yüzün. Bir yön belirtin (0-360).", - "Pond.stopTooltip": "Yüzmeyi bırak. Oyuncu durur.", - "Pond.healthTooltip": "Oyuncunun mevcut sağlığını döndürür (0 ölü, 100 sağlıklı).", - "Pond.speedTooltip": "Oynatıcının geçerli hızını döndürür (0 durdurulur, 100 tam hızdır).", - "Pond.locXTooltip": "Oynatıcının X koordinatını döndürür (0 sol kenar, 100 sağ kenardır).", - "Pond.locYTooltip": "Oynatıcının Y koordinatını döndürür (0 alt kenar, 100 üst kenardır).", - "Pond.logTooltip": "Tarayıcınızın konsoluna bir numara yazdırır.", - "Pond.docsTooltip": "Dil belgelerini görüntüleyin.", - "Pond.documentation": "Belgelendirme", + "Movie.setColour": "Rengi Ayarla", + "Movie.submitDisabled": "Filmin hareket etmiyor. İlginç bir şey yapmak için blokları kullan. Sonra filmini galeriye gönderebilirsin.", + "Movie.galleryTooltip": "Film galerisini aç.", + "Movie.galleryMsg": "Galeriye Göz At", + "Movie.submitTooltip": "Filmini galeriye gönder.", + "Movie.submitMsg": "Galeriye Gönder", + "Movie.helpLayer": "Arka plan dairesini programında en üste taşı. Böylece kişinin arkasında görünecektir.", + "Movie.helpText1": "Bu figürü çizmek için temel şekilleri kullan.", + "Movie.helpText2a": "Bu bir film seviyesi. Figürün kolunun ekran boyunca hareket etmesini sağlamalısın. Önizleme için Oynat düğmesine bas.", + "Movie.helpText2b": "Film oynarken, 'zaman' bloğunun değeri 0'dan 100'e sayar. Kolun 'y' konumunun 0'dan başlayıp 100'e gitmesini istediğin için bu kolay olmalı.", + "Movie.helpText3": "'Zaman' bloğu 0'dan 100'e sayıyor. Ama şimdi diğer kolun 'y' pozisyonunun 100'den başlayıp 0'a gitmesini istiyorsun. Yönü tersine çeviren basit bir matematik formülü bulabilir misin?", + "Movie.helpText4": "Bacakları çapraz yapmak için önceki seviyelerde öğrendiklerini kullan.", + "Movie.helpText5": "Kol için matematiksel formül biraz karmaşık. İşte cevabı:", + "Movie.helpText6": "Figüre birkaç el daha ekle.", + "Movie.helpText7": "Filmin ilk yarısı için küçük bir kafa çizmek amacıyla 'eğer' bloğunu kullan. İkinci yarısı içinse büyük bir kafa çiz.", + "Movie.helpText8": "Bacakları filmin ortasında zıt yönde hareket ettir.", + "Movie.helpText9": "Figürün arkasına doğru genişleyen bir daire çizebilir misin?", + "Movie.helpText10": "İstediğin her şeyin filmini yap. Keşfedebileceğin tonlarca yeni blok var. İyi eğlenceler!", + "Movie.helpText10Reddit": "Diğerlerinin filmlerini görmek için 'Galeriye Göz At' düğmesini kullan. İlginç bir film yaparsan, yayınlamak için 'Galeriye Gönder' düğmesini kullan.", + "Music.playNoteTooltip": "Belirtilen süre ve perdede bir nota çalar.", + "Music.playNote": "%1 süreyle %2 notasını çal", + "Music.restTooltip": "Belirtilen süre kadar bekler (sessizlik).", + "Music.restWholeTooltip": "Bütün bir nota süresi kadar bekler.", + "Music.rest": "Kalan Süre: %1", + "Music.setInstrumentTooltip": "Sonraki notaları çalmak için belirtilen enstrümana geçer.", + "Music.setInstrument": "Enstrümanı %1 olarak ayarla", + "Music.startTooltip": "'Programı Başlat' düğmesine tıklandığında içindeki blokları yürütür.", + "Music.start": "%1 Simgesine Tıklandığında", + "Music.pitchTooltip": "Bir notanın perdesi (C4 genellikle 7'dir).", + "Music.firstPart": "Birinci Kısım", + "Music.piano": "Piyano", + "Music.trumpet": "Trompet", + "Music.banjo": "Banço", + "Music.violin": "Keman", + "Music.guitar": "Gitar", + "Music.flute": "Flüt", + "Music.drum": "Davul", + "Music.choir": "Koro", + "Music.submitDisabled": "Programını durana kadar çalıştır. Ardından müziğini galeriye gönderebilirsin.", + "Music.galleryTooltip": "Müzik galerisini aç.", + "Music.galleryMsg": "Galeriye Göz At", + "Music.submitTooltip": "Müziğini galeriye gönder.", + "Music.submitMsg": "Galeriye Gönder", + "Music.helpUseFunctions": "Çözümün iyi, ama daha iyi olabilir. Tekrarlanan kod miktarını azaltmak için fonksiyonları kullan.", + "Music.helpUseInstruments": "Her başlangıç bloğunda farklı bir enstrüman kullanırsan müziğin daha güzel duyulur.", + "Music.helpText1": "'Frère Jacques' şarkısının ilk dört notasını yaz.", + "Music.helpText2a": "Bir 'fonksiyon' bloğu, kod bloklarını gruplamana ve sonra bunları birden fazla çalıştırmana olanak tanır.", + "Music.helpText2b": "'Frère Jacques'in ilk dört notasını çalmak için bir fonksiyon oluştur. Bu fonksiyonu iki kez çalıştır. Yeni nota bloğu ekleme.", + "Music.helpText3": "'Frère Jacques'in sonraki kısmı için ikinci bir fonksiyon oluştur. Sondaki nota daha uzun.", + "Music.helpText4": "'Frère Jacques'in sonraki kısmı için üçüncü bir fonksiyon oluştur. İlk dört nota daha kısa.", + "Music.helpText5": "'Frère Jacques' şarkısının tamamını tamamla.", + "Music.helpText6a": "Bu yeni blok, başka bir enstrümana geçmeni sağlar.", + "Music.helpText6b": "Melodini bir keman ile çal.", + "Music.helpText7a": "Bu yeni blok, sessiz bir bekleme süresi (gecikme) ekler.", + "Music.helpText7b": "İki bekleme bloğu olan ikinci bir başlangıç bloğu oluştur, sonra 'Frère Jacques'i çal.", + "Music.helpText8": "Her başlangıç bloğu 'Frère Jacques'i iki kez çalmalıdır.", + "Music.helpText9": "Her biri iki kez 'Frère Jacques' çalan dört başlangıç bloğu oluştur. Doğru sayıda bekleme bloğu eklediğinden emin ol.", + "Music.helpText10": "İstediğin her şeyi bestele! Keşfedebileceğin bir sürü yeni blok var. İyi eğlenceler!", + "Music.helpText10Reddit": "Başkalarının neler bestelediğini görmek için 'Galeriye Göz At' düğmesini kullan. İlginç bir şey oluşturursan, yayınlamak için 'Galeriye Gönder' düğmesini kullan.", + "Pond.scanTooltip": "Düşmanları tara. Bir yön (0-360) belirt. Bu yöndeki en yakın düşmana olan mesafeyi verir. Hiç düşman yoksa Sonsuzluk döndürür.", + "Pond.cannonTooltip": "Topu ateşle. Bir yön (0-360) ve bir menzil (0-70) belirle.", + "Pond.swimTooltip": "İleriye doğru yüz. Bir yön (0-360) belirt.", + "Pond.stopTooltip": "Yüzmeyi bırak. Oyuncu duracaktır.", + "Pond.healthTooltip": "Oyuncunun anlık sağlığını döndürür (0 ölü, 100 tam sağlıklı).", + "Pond.speedTooltip": "Oyuncunun anlık hızını döndürür (0 durmuş, 100 tam hız).", + "Pond.locXTooltip": "Oyuncunun X koordinatını döndürür (0 sol kenar, 100 sağ kenar).", + "Pond.locYTooltip": "Oyuncunun Y koordinatını döndürür (0 alt kenar, 100 üst kenar).", + "Pond.logTooltip": "Tarayıcının konsoluna bir sayı yazdırır.", + "Pond.docsTooltip": "Dil belgelerini görüntüle.", + "Pond.documentation": "Belgeler", "Pond.playerName": "Oyuncu", "Pond.targetName": "Hedef", - "Pond.pendulumName": "PendulumSarkaç", + "Pond.pendulumName": "Sarkaç", "Pond.scaredName": "Korkmuş", - "Pond.helpUseScan": "Çözümünüz işe yarıyor, ancak daha iyisini yapabilirsiniz. Topa ne kadar uzak ateş edileceğini söylemek için 'scan'ı kullanın.", - "Pond.helpText1": "Hedefi vurmak için 'cannon' komutunu kullanın. İlk parametre açı, ikinci parametre aralıktır. Doğru kombinasyonu bul.", - "Pond.helpText2": "Bu hedefe birçok kez vurulması gerekiyor. Süresiz bir şey yapmak için 'while (true)' döngüsünü kullanın.", - "Pond.helpText3a": "Bu rakip ileri geri hareket ederek vurmayı zorlaştırıyor. 'scan' ifadesi, rakibe tam olarak belirtilen yönde döndürür.", - "Pond.helpText3b": "Bu menzil 'cannon' komutunun tam olarak ateş etmesi için gereken şeydir.", - "Pond.helpText4": "Bu rakip topu kullanmak için çok uzak (70 metre sınırı var). Bunun yerine, rakibe doğru yüzmeye başlamak ve ona çarpmak için 'swim' komutunu kullanın.", - "Pond.helpText5": "Bu rakip aynı zamanda topu kullanamayacak kadar uzakta. Ancak bir çarpışmadan kurtulmak için çok zayıfsın. Yatay konumunuz 50'den azken rakibe doğru yüzün. Sonra 'durun' ve topu kullanın.", - "Pond.helpText6": "Bu rakip vurulduğunda uzaklaşacak. Menzil dışındaysa (70 metre) yüzün.", + "Pond.helpUseScan": "Çözümün çalışıyor, ancak daha iyisini yapabilirsin. Topu ne kadar uzağa ateşleyeceğini belirlemek için 'tara' (scan) komutunu kullan.", + "Pond.helpText1": "Hedefi vurmak için 'top at' (cannon) komutunu kullan. İlk parametre açı, ikincisi menzil. Doğru kombinasyonu bul.", + "Pond.helpText2": "Bu hedefin birden çok kez vurulması gerekiyor. Sonsuz döngü oluşturmak için 'while (true)' döngüsünü kullan.", + "Pond.helpText3a": "Bu rakip ileri geri hareket ediyor, bu yüzden vurmak zor. 'Tara' (scan) komutu, tam olarak belirtilen yöndeki rakibe olan mesafeyi döndürür.", + "Pond.helpText3b": "Bu menzil, 'top at' (cannon) komutunun tam isabet için ihtiyaç duyduğu değerdir.", + "Pond.helpText4": "Bu rakip top kullanmak için çok uzakta (70 metrelik sınır var). Bunun yerine, rakibe doğru yüzmeye başlamak ve ona çarpmak için 'yüz' (swim) komutunu kullan.", + "Pond.helpText5": "Bu rakip de top kullanmak için çok uzakta. Ancak çarpışmadan kurtulmak için çok zayıfsın. Yatay konumun 50'den küçükken rakibe doğru yüz. Sonra 'dur' ve topu kullan.", + "Pond.helpText6": "Bu rakip vurulduğunda uzaklaşacak. Menzil dışındaysa (70 metreden uzakta) yüz.", "Gallery": "Galeri" }