diff --git a/.github/workflows/process_translations.yml b/.github/workflows/process_translations.yml new file mode 100644 index 0000000..d54fdee --- /dev/null +++ b/.github/workflows/process_translations.yml @@ -0,0 +1,69 @@ +name: Process translations + +on: + pull_request_target: + push: + branches: [master] + +jobs: + process_translations: + permissions: + actions: read + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Checkout base + uses: actions/checkout@v4 + with: + fetch-depth: '100' + - name: Checkout pr + if: ${{ github.event_name == 'pull_request_target' }} + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repository.full_name }} + ref: ${{ github.event.pull_request.head.sha }} + path: ./pr + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + run: | + sudo apt update + sudo apt install gettext + tar xzf mdpo-files/wheels.tar.gz + python3 -m pip install wheels/*.whl + rm -rf wheels + - name: Process master changes + if: ${{ github.event_name == 'push' }} + env: + PROJECTS_DIR: '.' + BEFORE_PUSH_COMMIT_SHA: ${{ github.event.before }} + run: python3 scripts/process_master_changes.py + - name: Process translations + env: + PROJECTS_DIR: ${{ (github.event_name == 'push' && '.') || './pr' }} + run: python3 scripts/process_translations.py + - name: 'Comment on PR' + if: ${{ github.event_name == 'pull_request_target' }} + uses: actions/github-script@v7 + with: + script: | + let fs = require('fs'); + let comment_body = fs.readFileSync('result.txt', {'encoding': 'utf-8'}); + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment_body + }); + - name: Commit changes + if: ${{ github.event_name == 'push' }} + run: | + rm -rf ./pr result.txt + git config user.email 'contact@sooslandia.ru' + git config user.name 'Sooslandia github action' + git add . + git commit -m "Update translations" || : + git push || : diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4468906 --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +#*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/Agsharp/AudioForms.resx b/Agsharp/AudioForms.resx new file mode 100644 index 0000000..76ac7f4 --- /dev/null +++ b/Agsharp/AudioForms.resx @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + button + + + unavailable + + + checkbox + + + checked + + + unchecked + + + Cap + + + copied + + + cut + + + empty + + + edit + + + no selection + + + pasted + + + selected + + + selection removed + + + {0} characters selected + + + {0} characters unselected + + + unselected + + + {0} of {1} + + + list + + + slider + + + tab panel + + + tab + + \ No newline at end of file diff --git a/Agsharp/AudioForms.ru.resx b/Agsharp/AudioForms.ru.resx new file mode 100644 index 0000000..06e2032 --- /dev/null +++ b/Agsharp/AudioForms.ru.resx @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + кнопка + + + недоступно + + + флажок + + + отмечено + + + не отмечено + + + большая + + + скопировано + + + вырезано + + + пусто + + + редактор + + + нет выделения + + + вставлено + + + выделено + + + выделение удалено + + + {0} символов выделено + + + {0} символов не выделено + + + не выделено + + + {0} из {1} + + + список + + + ползунок + + + панель вкладок + + + вкладка + + \ No newline at end of file diff --git a/BallBouncer/docs/en/changelog.md b/BallBouncer/docs/en/changelog.md new file mode 100644 index 0000000..cc0658e --- /dev/null +++ b/BallBouncer/docs/en/changelog.md @@ -0,0 +1,38 @@ +% changelog + +## 1.1.1 + +### Fixes + +- Fixed a critical bug that occurred when the first person mode and the ball watch mode were active at the same time. +- Fixed some other minor bugs. + +## 1.1.0 + +This version is focused on improving the user experiance: Successful bat hit sound, first person camera view, alternative bat swing keys, etc. + +### Added + +- The game now supports translations that are missing one or more strings. If a string is not found, the game falls back to English localization strings. +- In ball watching mode, a background sound has been attached to the ceiling, which will help make watching more spectacular. +- Added a sound to indicate when the bat succesfully hits the ball. By default, the notification is disabled; it is enabled in the settings, on the "Behavior" tab. +- Implemented first-person camera mode. To switch between modes, press v while playing. +- Errors during the update are now written to a file that will be located in the userData/errorLogs folder. +- Added temporary alternative keys for making horizontal and vertical bat swings. + - For a horizontal swing, use the e key, for a vertical swing, use the r key. + - This solution is temporary and remains until the key config is implemented. +- Now items with an available reward in the statistics list are at the beginning of the list. + +### Changes + +- Increased points received for perfect strike streeks, ball bounces off the ceiling and ball with object collisions streeks. +- Documentation has been updated to take new features into account. + +### Fixes + +- Increased the volume of the landing sound after a jump. +- Now the leader's aura does not increase the points lost due to penalties. + +## 1.0.0 + +Initial release. \ No newline at end of file diff --git a/BallBouncer/docs/en/readme.md b/BallBouncer/docs/en/readme.md new file mode 100644 index 0000000..45ed336 --- /dev/null +++ b/BallBouncer/docs/en/readme.md @@ -0,0 +1,108 @@ +% "Ball bouncer" documentation + +This help file explains how to navigate the menus and screens of the application, and also describes the gameplay in detail with most of the nuances. If you want to figure it out yourself, we recommend that you immediately go and familiarize yourself with the table of control keys. + +## System requirements + +64 bit windows 10 or later. + +64 bit ubuntu 22 or later. + +## First steps + +When the game runs for the first time you will have to select a language. After making your selection, a welcome screen will appear. As soon as you make your selection our logo will start playing. You can skip it by pressing the enter key. After the logo is skipped or finishes playing you will be taken to the main menu of the game. We encourage you to familiarize yourself with its contents. +If you want to learn the game sounds, you need to select “Settings” in the main menu, then on the “General” tab click on the corresponding button. +On the “Sound” tab you can adjust the volume of music and sounds, change the sound panning mode And disable some alerts. on the "Behavior" tab. + +The game has a training mode where you can spend unlimited time and where all penalties are disabled. In this mode, a sound will be played when you can hit the ball, which you can listen to in the learn sounds menu. The sound will play when the ball has bounced at least once off the floor to teach you to sense the moment for the perfect strike. In training mode, game statistics are not collected, and points are not scored. +If you don't like the sound of the ball, you can change it in the settings, on the "Sound" tab, choosing from five proposed options. This can be done directly during the game by going to the settings from the pause menu. The ball sound will change as soon as you unpause the game. + +## Navigation + +Moving through menus is done by using the up and down arrows. Enter selects an item, while home and end keys do their respective job by setting you on the first or last menu item. + +Screen navigation(settings, shop, profile, etc.),: tab/shift+tab forward/back, activate controls by pressing enter or spacebar. Move through the lists with up and down arrows, change the value of the sliders with arrows. Tab selection is done by pressing left/right arrow keys. + +On the shop screen, by pressing enter on the item you have selected, you can open reference information that describes this item. +In your profile, on the "Statistics" tab, by pressing enter on a list item you can receive a reward, if available. +On the “Skills” tab, by clicking on an item, you can get detailed information about the skill. + +All menus and screens, excluding the main menu and the last game results menu, can be closed with the escape key. + +## Game process + +### How to play + +After selecting “Start game” in the main menu, the starting countdown will begin. You will then be able to control the character. +At the beginning of the game the character is in the center of the field. You can move it using the left and right arrows. Initially the camera is in the field-centered view, but you can switch it to first person view and back by pressing v. By pressing the C key, you will hear a sound positioned at the character's location. This will help you better understand your character whereabouts when trying to hit the ball. With the left or right shift you can set the direction the character is facing. When moving, the facing direction is set automatically. + +First of all, you need to throw the ball so that it appears on the playing field. You can do this by pressing the f key. +After the ball is on the field, you will hear a tone which will move left and right, indicating movement of the ball in the horizontal plane. The tone's pitch and volume will also change, indicating vertical ball movement. Decreasing volume and rising pitch mean that the ball is moving upwards, while increasing volume and lowering pitch mean the opposite. + +There are two types of throw: + ++ Normal. Performed by pressing the f key. The ball begins to fly in an arc in the direction the character is facing. ++ Vertical. Performed by holding down the alt key and pressing f. The ball starts flying along the vertical axis, but also moves slightly along the horizontal axis in the direction the character is facing. + +The goal of the game is to destroy as many objects as possible, gaining points along the way. The ball always tends to fall down, and you need to hit it back with a bat. + +The ball can be hit in two ways: + ++ Spacebar key. In this case, the ball will receive a strong horizontal impulse in the direction the character is facing. ++ Hold down the alt key and press spacebar. The ball will receive a strong vertical impulse, maintaining a horizontal flight direction regardless of the character's facing. + +You can only hit the ball while being near it. After each swing of the bat, the character restores the force of the blow. Once it is fully restored, you will hear an alert, the sound of which can be heard in the learn sounds menu. You can also toggle the sound of successful bat hits in the options on the behavior tab. + +You need to strive to always make perfect strikes, as you will be awarded bonus points for them. A perfect strike occurs if the ball bounces off the ground only once. For the second and subsequent bounces, you will lose points. The more the ball bounces off the ground, the more points you lose. Therefore, try to hit the ball up as quickly as possible. +Standing in one place for a long time is also not worth it, since if you don’t move for ten or more seconds, you will also lose points. +At the beginning of the game, do not hold the ball in your hands for long, as each second of holding the ball will result in an increasing penalty. The same applies to the caught ball. +For the fifth and subsequent misses with the bat a penalty is assessed. +For colliding the ball with an object, as well as for destroying the object, you get points. If the ball bounces off the ceiling, you will also get bonus points. +After colliding with an object or destroying it, a streak begins. There are separate streaks for collisions and destructions. Each subsequent collision or destruction brings more points. But if the streak did not last more than six seconds, it is reset. The same applies to ball bounces from the ceiling. After making a perfect strike, a streak also begins. For each new perfect strike you make, you get significantly more points than the previous one. If the ball bounces off the floor more than once, the streak ends and you have to start over. +Once destroyed, objects will fall to the ground, causing inconvenience with loud sounds and adding to the chaos. + +During the game, you can watch not only the entire picture of the playing field, but also switch to the ball watch mode. To activate this mode, hold down the G key. Ball Watch mode will be active as long as you hold G and will turn off as soon as you release it. +The game can be paused by pressing the escape key. When activated, you will be taken to the pause menu, where you can see the time until the end of the gaming session, the number of points scored and other information. You can also access the settings from the pause menu, or interrupt the game. +Alternatively, press t to see the time and p to see the points during the game. + +If you allow your points to drop to minus one thousand, the gaming session will end early. Coins will not be credited, but you do not lose anything else. +A gaming session lasts five minutes. This time can be increased by improving the time aura in the profile. +After the game session ends, a sound animation of coins being added will begin playing, which can be skipped by pressing the enter key. +Then the last game results menu will open, where you can find out how many points were ultimately scored and coins received. After closing the menu by pressing enter, you may be shown a notification about an available reward for reaching a statistic milestone. The display of the results menu for the last game and notifications about available rewards can be disabled in the settings, on the “Behavior” tab. + +The game also has skills that are fully described in the game. To use a skill during the game, you need to assign it to one of ten slots. Assigning is done in your profile, on the “Skills” tab. Slots are accessed using keys from 1 (first slot) to 0 (tenth slot). + +In the store you can unlock the mechanics of jumping and catching the ball. These two mechanics are also fully described in the game, including the keys to use. They are also present in the table of controls. + +### control keys + +| Action | Key/keys | +| :--- | :--- | +| Character movement right/left | Right/Left arrows or d/a keys| +| Play sound at character's position | c | +| Set character facing right/left | right/left shift | +| Swing the bat (hit the ball with a strong horizontal impulse) | Spacebar or e key | +| Swing the bat (hit the ball with strong vertical impulse) | alt+space or r +| Throw the ball in an arc | f | +| Throw the ball vertically | alt+f | +| Switch the camera to ball watch mode | Hold down the g key | +| Switch the camera to normal mode | Release the g key | +| Switch camera mode between first person mode and field-centered view mode | v | +| Jump (if the mechanic is unlocked) | Up arrow or w key | +| Catch the ball (if the mechanic is unlocked) | left or right control | +| Select a skill slot from 1 to 10 (if the skill is set) | keys 1 to 0 | +| Cancel skill slot selection | grave | +| Find out the time until the end of the gaming session | t | +| Find out the current number of points scored | p | +| Pause the game | escape | + +## Conclusion + +We are glad that you got interested in this project and we hope that you will like it. Destroy and have fun! + +Subscribe to our [channel](https://t.me/sooslandia) on telegram to receive the latest news about updates to this game, as well as about other interesting projects. + +Join the [channel chat](https://t.me/sooslandiadiscussion), where you can share your opinion about the game, report any bugs, or suggest your idea. +If you don't use telegram, you can write to [our email](mailto://contact@sooslandia.ru), but a quick response is not guaranteed. + +All the best, Sooslandia-games. \ No newline at end of file diff --git a/BallBouncer/docs/ru/changelog.md b/BallBouncer/docs/ru/changelog.md new file mode 100644 index 0000000..c4c7603 --- /dev/null +++ b/BallBouncer/docs/ru/changelog.md @@ -0,0 +1,38 @@ +% Список изменений + +## 1.1.1 + +### Исправления + +- Исправлена критическая ошибка, происходившая при одновременных активных режимах камеры от первого лица и наблюдения за мячом. +- Исправлены другие мелкие ошибки. + +## 1.1.0 + +В этой версии было уделено внимание добавлению различных улучшающих пользовательский опыт нововведений и изменений, таких как звук, воспроизводящийся при попадании битой по мячу, режим камеры от первого лица, добавление альтернативных клавиш для совершения ударов битой и другое. + +### Нововведения + +- Игра теперь поддерживает переводы, в которых отсутствует один или несколько ключей. При их отсутствии будут использованы строки английской локализации. +- В режиме наблюдения за мячом к потолку был прикреплён фоновый звук, что поможет сделать наблюдение более зрелищным. +- Добавлен звук оповещения о попадании битой по мячу. По умолчанию оповещение отключено, включается в настройках, на вкладке "Поведение". +- Реализован режим камеры от первого лица. Чтобы переключаться между режимами, во время игры нажимайте клавишу v. +- Ошибки во время обновления теперь записываются в файл, который будет находиться в папке userData/errorLogs. +- Добавлены временные альтернативные клавиши для совершения горизонтальных и вертикальных взмахов битой. + - Для горизонтального взмаха используйте клавишу e, для вертикального - r. + - Это решение является временным и остаётся до момента, пока не будет добавлен конфигуратор сочитаний клавиш, где самостоятельно можно будет изменить или добавить любое сочитание на любое игровое действие. +- Теперь пункты с доступной наградой в списке статистики находятся в начале списка. + +### Изменения + +- Увеличено количество получаемых очков за серии столкновений мяча с объектами, отскоков от потолка и идеальных ударов. +- Обновлена документация с учётом нововведений. + +### Исправления + +- Увеличена громкость звука приземления после прыжка. +- Теперь аура лидера не увеличивает очки, теряемые при штрафах. + +## 1.0.0 + +Первый выпуск. \ No newline at end of file diff --git a/BallBouncer/docs/ru/readme.md b/BallBouncer/docs/ru/readme.md new file mode 100644 index 0000000..d5920df --- /dev/null +++ b/BallBouncer/docs/ru/readme.md @@ -0,0 +1,110 @@ +% документация к игре "Ball bouncer" + +В этом справочном файле объясняются способы навигации по меню и экранам приложения, а также подробно описывается процесс игры с большей частью нюансов. Если вы хотите разобраться самостоятельно, советуем сразу перейти и ознакомиться с таблицей клавиш управления. + +## Системные требования + +64 разрядная windows 10 или новее. + +64 разрядная ubuntu 22 или новее. + +## Первые шаги + +При первом запуске требуется выбрать язык. После выбора появится приветственный экран, а нажав продолжить, после проигрыша нашего лого, которое можно пропустить, нажав клавишу enter, вы попадёте в главное меню игры. С его содержимым предлагаем ознакомиться самостоятельно. +Если вы хотите прослушать звуки различных оповещений, вам нужно в главном меню выбрать пункт "Настройки", затем на вкладке "Общие" нажать на соответствующую кнопку. +Так же на вкладке "Звук" вы можете отрегулировать громкость музыки и игры, изменить режим панорамирования звуков. А на вкладке "Поведение" отключить некоторые оповещения. + +В игре есть тренировочный режим, где вы можете проводить неограниченное время, и где все штрафы отключены. В этом режиме, если вы можете отбить мяч, будет проигрываться звук, который можно прослушать в меню изучения звуков. Звук будет играть в том случае, когда мяч отскочил хотя бы раз от пола, чтобы научить вас чувствовать момент для идеального удара. В тренировочном режиме не собирается игровая статистика, а также не происходит набор очков. + +Если вам не нравится звук мяча, вы можете поменять его в настройках, на вкладке "Звук", выбрав из пяти предложенных вариантов. Это можно сделать прямо во время игры, попав в настройки из меню паузы. Звук мяча изменится, как только вы снимите игру с паузы. + +## Навигация + +Перемещение по меню осуществляется стрелками вверх и вниз, выбор пункта - клавиша enter. Перейти в начало меню можно с помощью клавиши home, а в конец - end. + +Перемещение по экрану настроек, магазину, профилю и другим осуществляется клавишами tab - вперёд, shift+tab - назад. Активация кнопок - клавиша enter или пробел. Перемещение по спискам - стрелки вверх и вниз. Изменение позиции ползунков - клавиши стрелки. Переключение вкладок на панели осуществляется стрелками вправо и влево. + +На экране магазина, нажав enter на выбранном вами товаре, можно открыть справочную информацию, где описывается данный товар. +В профиле, на вкладке "Статистика", нажав enter на пункте списка можно получить награду, если она доступна. +На вкладке "Умения", нажав на пункт, можно получить подробную информацию об умении. + +Все меню и экраны, исключая главное меню и меню результатов прошедшей игры, можно закрыть клавишей escape. + +## Игровой процесс + +### Как играть + +После выбора пункта "Начать игру" в главном меню, начнётся стартовый отсчёт. Затем вы сможете контролировать персонажа. +В начале игры он находится в центре поля. Перемещать его можно стрелками вправо и влево. Камера изначально переключена в режим обзора всего поля, но можно переключить её в режим от первого лица клавишей v. Нажав на клавишу C, вы услышите звук, спозиционированный на местонахождения персонажа. Это поможет вам лучше чувствовать персонажа при попытках отбития мяча. Левым или правым шифтом вы можете задать направление взгляда персонажа. При движении направление задаётся автоматически. + +Первым делом, вам требуется бросить мяч, чтобы он появился на игровом поле. Сделать это можно, нажав клавишу f. +После того, как мяч окажется на поле, вы будете слышать однотонный звук, который будет перемещаться слева направо в горизонтальной плоскости, и менять свою тональность и громкость, как бы отдаляясь от вас, при изменении высоты. Чем выше находится мяч, тем выше тональность звука мяча вы будете слышать. + +Существует два типа броска: + ++ Обычный. Выполняется нажатием клавиши f. Мяч начинает полёт по дуге в направлении взгляда персонажа. ++ Вертикальный. Выполняется с зажатой клавишей alt и нажатием f. Мяч начинает полёт по вертикальной оси, но так же немного движется и по горизонтальной в направлении взгляда персонажа. + +Цель игры - уничтожить как можно больше объектов, попутно набирая очки. Мяч всегда стремится упасть вниз, а вам требуется его отбивать битой. + +Мяч можно отбить двумя способами: + ++ Клавишей пробел. В таком случае мяч получит сильный горизонтальный импульс в направлении взгляда персонажа. ++ Зажав клавишу alt и нажав пробел. Мяч получит сильный вертикальный импульс, сохраняя направление горизонтального полёта вне зависимости от взгляда персонажа. + +Отбить мяч можно только находясь рядом с ним. После каждого взмаха битой, персонаж восстанавливает силу удара. После того, как сила будет полностью восстановлена, вы услышите оповещение, звук которого можно прослушать в меню изучения звуков. А так же, на вкладке "Поведение", можно включить звуковое оповещение об успешном попадании битой по мячу. + +Вам нужно стремиться всегда совершать идеальные удары, так как за них вам начисляются бонусные очки. Идеальный удар проходит в том случае, если мяч отскочил от земли только один раз. За второй и выше отскоки, вы будете терять очки. Чем больше отскоков от земли совершил мяч, тем больше вы теряете очков. Поэтому стремитесь отбить мяч вверх как можно быстрее. +Стоять долго на одном месте тоже не стоит, так как за отсутствие движения десять и более секунд вы тоже будете терять очки. +В начале игры не держите долго мяч в руках, так как каждую секунду удержания мяча начисляется нарастающий штраф. То же самое касается и пойманного мяча. +За пятый и последующие промахи начисляется штраф. +За столкновение мяча с объектом, а также за уничтожения объекта вы получаете очки. Если мяч отскочит от потолка, вы тоже получите бонусные очки. +После столкновения с объектом или его уничтожения начинается серия. Для столкновений и уничтожений серия своя. Каждое последующее столкновение или уничтожение приносит больше очков. Но если серия не продолжалась более шести секунд, она сбрасывается. То же касается отскоков мяча от потолка. После совершения идеального удара так же начинается своя серия. За каждый новый совершённый идеальный удар вы получаете значительно больше очков, чем за предыдущий. Если мяч отскочил более одного раза от пола, серия прекращается, и вам придётся начинать её сначала. +После уничтожения объекты будут падать на землю, создавая неудобство громкими звуками и добавляя хаоса в происходящее. + +Во время игры вы можете наблюдать не только за всей картиной игрового поля, но и переключиться в режим наблюдения за мячом. Для активации этого режима удерживайте клавишу G. Режим наблюдения за мячом будет активен, пока вы удерживаете G, и отключится сразу же, как только вы её отпустите. +Игру можно поставить на паузу, нажав клавишу escape. После нажатия вы попадёте в меню паузы, где сможете посмотреть время до окончания игровой сессии, количество набранных очков и другую информацию. Также из меню паузы можно попасть в настройки, либо же прервать игру. +Но время до окончания игровой сессии и текущее очки можно посмотреть не только в меню паузы. Нажмите клавишу t для того, чтобы узнать о времени, а p - чтобы узнать о набранных очках. + +Если вы допустите уменьшение очков до минус одной тысячи, игровая сессия окончится досрочно. Монеты не будут начислены, но вы ничего больше не теряете. +Игровая сессия длится пять минут. Это время можно увеличить, улучшив ауру времени в профиле. +После завершения игровой сессии, начнётся проигрывание звуковой анимации начисления монет, которую можно пропустить, нажав клавишу enter. +Затем откроется меню результатов прошедшей игры, где вы можете узнать сколько в итоге было набрано очков и получено монет. Закрыв меню нажатием клавиши enter, вам может быть показано уведомление о доступной награде за достижение вехи статистики. Показ меню результатов прошедшей игры и уведомления о доступной награде можно отключить в настройках, на вкладке "Поведение". + +В игре также есть умения, которые полностью описаны в игре. Чтобы использовать умения во время игры, вам требуется назначить его в один из десяти слотов. Назначить можно в профиле, на вкладке "Умения". Доступ к слотам осуществляется клавишами от 1 (первый слот) до 0 (десятый слот). + +В магазине вы можете разблокировать механику прыжка и ловли мяча. Эти две механики также полностью описаны в игре, включая клавиши использования. В таблице клавиш управления они тоже присутствуют. + +### клавиши управления + +| Действие | Клавиша / клавиши | +| :--- | :--- | +| Движение персонажа вправо/влево | Стрелки вправо/влево или клавиши d/a | +| Проиграть звук на позиции персонажа | c | +| Повернуть персонажа вправо/влево | правый/левый шифт | +| Взмах битой (отбить мяч с сильным горизонтальным импульсом) | Пробел, клавиша s или e | +| Взмах битой (отбить мяч с сильным вертикальным импульсом) | alt+пробел, alt+s или r | +| Бросить мяч по дуге | f | +| Бросить мяч вертикально | alt+f | +| Переключить камеру в режим наблюдения за мячом | Зажать клавишу g | +| Переключить камеру в обычный режим | Отпустить клавишу g | +| Переключить режим камеры между режимом от первого лица и режимом наблюдения за всем полем) | v | +| Прыжок (если механика разблокирована) | Стрелка вверх или клавиша w | +| Поймать мяч (если механика разблокирована) | левый или правый control | +| Выбрать слот умения от 1 до 10 (если умение установлено) | клавиши от 1 до 0 | +| Отменить выбор слота умения | Обратный апостроф | +| Узнать время до окончания игровой сессии | t | +| Узнать текущее количество набранных очков | p | +| Поставить игру на паузу | escape | + +## Заключение + +Мы рады, что вы заинтересовались этим проектом и будем надеяться, что он придётся вам по душе. Разрушайте и развлекайтесь! + +Подписывайтесь на наш [канал](https://t.me/sooslandia_ru) в telegram, чтобы получать свежие новости об обновлениях этой игры, а так же о других, не менее интересных проектах. + +Присоединяйтесь к [чату канала](https://t.me/sooslandiadiscussionru), где вы можете поделиться своим мнением об игре, сообщить о возникших ошибках или предложить свою идею. + +Если вы не пользуетесь telegram, вы можете написать на [нашу почту](mailto://contact@sooslandia.ru), но быстрый ответ не гарантируется. + +Всего наилучшего, Sooslandia-games. \ No newline at end of file diff --git a/BallBouncer/english.lng b/BallBouncer/english.lng new file mode 100644 index 0000000..c52d4d2 --- /dev/null +++ b/BallBouncer/english.lng @@ -0,0 +1,217 @@ +{ + "AchievementPoints": "Achievement points: %1.", + "AchievementPointsTotal": "%1 achievement points received in total.", + "AuraLeaderText": "Leader's aura: level %1. For each level, adds one percent to the points received.", + "AuraTimeText": "Time Aura: level %1. For each level, adds 5 seconds to the maximum playing time.", + "AuraUpgradeButton": "Upgrade for %1 achievement points", + "BallLoopSelectionMenuSound": "Sound %1.", + "BallLoopSelectionMenuTitle": "Select the ball sound.", + "BallLoopSelectionMenuTitleWithHint": "Select the ball sound. Use the up and down arrows to move. To listen to the sound, press Spacebar. To confirm your selection, press enter.", + "CheckUpdatesScreenChecking": "Checking...", + "CheckUpdatesScreenNoUpdates": "You have the latest version.", + "Culture": "en", + "FileSizeBytes": "%1 b", + "FileSizeGigabytes": "%1 GB", + "FileSizeKilobytes": "%1 kb", + "FileSizeMegabytes": "%1 mb", + "GamePoints": "%1 points scored in this game.", + "GameResultScreenCoins": "Got %1 coins.", + "GameResultScreenPoints": "%1 points scored.", + "GameResultScreenTitle": "Results of the last game.", + "GameResultScreenTitleWithHint": "Results of the last game. Use the down and up arrows to navigate through the menu. To close the menu, press enter at any item.", + "GameRewardAvailableDialog": "A reward is available! Get it in your profile on the statistics tab!", + "GameTimeToEnd": "%1 seconds left until the end of the game.", + "IncompatableSaveMessage": "The loaded save belongs to a newer version of the game\r\nand contains data that will be lost.\r\nTherefore, the save is considered incompatible.\r\nThe game will be closed.", + "IncompatableSaveTitle": "Incompatible save", + "Language": "english", + "LearnSoundsBatHitSuccess": "Successfully hit the ball with the bat.", + "LearnSoundsCeilBounce": "The ball bounced off the ceiling.", + "LearnSoundsFineStanding": "You are starting to be fined for standing in one place for too long.", + "LearnSoundsFineSwinging": "Penalty for frequent misses with the bat on the ball.", + "LearnSoundsForceFull": "character's blow force has been fully restored.", + "LearnSoundsObjectDestroy": "Object destruction alert.", + "LearnSoundsObjectHit": "Ball hit an object alert.", + "LearnSoundsPerfectHit": "A perfect strike.", + "LearnSoundsScoresUnderNeg500": "The points dropped below minus five hundred.", + "LearnSoundsTitle": "Learn sounds menu.", + "LearnSoundsTitleWithHint": "Learn sounds menu. Use the up and down arrows to move. To listen to the sound, press enter. To exit the menu, press the corresponding menu item or the escape key.", + "MainMenuExitGame": "Exit the game.", + "MainMenuOptions": "Settings.", + "MainMenuProfile": "Profile.", + "MainMenuStartGame": "Start game.", + "MainMenuStore": "Shop.", + "MainMenuTitle": "Main menu.", + "MainMenuTitleWithHint": "Main menu. Use the up and down arrows to move. To select a menu item, press enter.", + "ModeSelectionMenuBack": "Return to main menu.", + "ModeSelectionMenuNormalMode": "Normal game.", + "ModeSelectionMenuPracticeMode": "Training mode.", + "ModeSelectionMenuTitle": "Select a game mode.", + "ModeSelectionMenuTitleWithHint": "Select a game mode. Use the up and down arrows to move. To confirm your selection, press enter.", + "NumberNotationsEmpty": "No notation.", + "NumberNotationsEngineering": "Engineering.", + "NumberNotationsLetter": "Letter.", + "NumberNotationsScientific": "Scientific.", + "NumberNotationsStandard": "Standard.", + "OptionsScreenAllowOverMaxVolume": "Allow audio volume boost up to two hundred percent", + "OptionsScreenAllowPlayObjectNotifySounds": "Play alert sounds when the ball collides with or destroys objects on the field", + "OptionsScreenBatHitSuccessSound": "Play a sound when the ball is successfully hit with the bat", + "OptionsScreenCatigoriesBehaviour": "Behavior", + "OptionsScreenCatigoriesGeneral": "General", + "OptionsScreenCatigoriesSound": "Sound", + "OptionsScreenChangeBallLoopSoundButton": "Change ball sound", + "OptionsScreenChangeLanguageButton": "Change language", + "OptionsScreenCheckForUpdates": "Check for updates", + "OptionsScreenCheckForUpdatesOnStartup": "Check for updates on startup", + "OptionsScreenCloseButton": "Close", + "OptionsScreenDisplayTitleHints": "Show navigation hints in menu titles", + "OptionsScreenGameVolumeSlider": "Game volume", + "OptionsScreenLearnSounds": "Learn game sounds", + "OptionsScreenMusicVolumeSlider": "Music volume", + "OptionsScreenNumberNotationSelector": "Notation for displaying numbers", + "OptionsScreenPauseGameWhenWindowLostsFocus": "Pause the game when switching to another window", + "OptionsScreenPrefereSapi": "Prefer sapi to screen reader", + "OptionsScreenShowResultMenu": "Show results menu at the end of the game", + "OptionsScreenShowRewardAvailableDialog": "Show notification if reward is available at end of game", + "OptionsScreenShowUpdateCheckErrorOnStartup": "Show an error if it was not possible to check for updates at startup", + "OptionsScreenSoundPanRepresentationMode": "Sound Panning Mode", + "OptionsScreenSoundPanRepresentationModeNarrowedCenter": "Narrowed center", + "OptionsScreenSoundPanRepresentationModeNormal": "Normal", + "OptionsScreenTitle": "Settings screen.", + "OptionsScreenTitleWithHint": "Settings screen. Use tab and shift+tab to move forward and backward. Use the arrow keys to change the position of the sliders. To activate the selected item, press enter or spacebar.", + "PauseMenuAbortGame": "Abort the game and return to the main menu.", + "PauseMenuContinueGame": "Continue game.", + "PauseMenuGameInfo": "%1 points scored in this game. The ball collided with objects %2 times, destroyed %3 objects, and bounced off the floor %4 times. You hit the ball %5 times.", + "PauseMenuPracticeModeActive": "Training mode is active.", + "PauseMenuTimeToEnd": "The game will end in %1 seconds.", + "PauseMenuTitle": "Pause menu.", + "PauseMenuTitleWithHint": "Pause menu. Use the up and down arrows to move. To activate the selected item, press enter.", + "ProfileAurasPageTitle": "Auras", + "ProfileSkillsPageTitle": "Skills", + "ProfileStatsPageTitle": "Statistics", + "ProfileTitle": "Your profile.", + "ProfileTitleWithHint": "Your profile. Use tab and shift+tab to move forwards and backwards. To receive a reward in statistics, press enter on the item with an available reward.", + "SkillsBallLeapLevelInfoText": "Level %1. The character makes a rapid dash towards the ball, located no higher than %2 cells above the ground, The cooldown interval is %3 seconds.", + "SkillsBallLeapName": "furious leap", + "SkillsBallLeapNameFull": "Furious leap.", + "SkillsBallLeapText": "Furious leap: level %1/4. The character makes a rapid dash towards the ball, located no higher than %2 cells above the ground, cooldown interval is %3 seconds.", + "SkillsEmpty": "You haven't unlocked any skills yet.", + "SkillsImmaterialityLevelInfoText": "Level %1. The ball goes immaterial for %2 seconds, cooldown interval is %3 seconds.", + "SkillsImmaterialityName": "immateriality", + "SkillsImmaterialityNameFull": "immateriality", + "SkillsImmaterialityText": "Immateriality: level %1/3. The ball goes immaterial for %2 seconds, cooldown interval is %3 seconds.", + "SkillsLevelInfo": "List of skill effects by level.", + "SkillsListTitle": "Unlocked Skills", + "SkillSlotRecharging": "%1 seconds before skill cooldown.", + "SkillSlotSelectCanceled": "Selection canceled", + "SkillSlotSelected": "Skill %1 is selected.", + "SkillSlotSetterMenuBack": "Go back.", + "SkillSlotSetterMenuTitle": "Select a skill to install in the slot.", + "SkillSlotSetterMenuTitleWithHint": "Select a skill to install in the slot. Use the up and down arrows to select and press enter to confirm your selection.", + "SkillSlotsTitle": "Slots", + "SkillSlotText": "Slot %1. %2", + "SkillsMagnetismLevelInfoText": "Level %1. The ball receives an effect that attracts it to nearby objects for %2 seconds, cooldown interval is %3 seconds.", + "SkillsMagnetismName": "magnetism", + "SkillsMagnetismNameFull": "Magnetism.", + "SkillsMagnetismText": "Magnetism: level %1/4. The ball receives an effect that attracts it to nearby objects for %2 seconds, cooldown interval is %3 seconds.", + "SkillsNoneNameFull": "Not set.", + "SkillsObjectSpawnerLevelInfoText": "Level %1. The swing of the bat creates %2 objects on the field, cooldown interval is %3 seconds.", + "SkillsObjectSpawnerName": "creator's stroke", + "SkillsObjectSpawnerNameFull": "Creator's stroke.", + "SkillsObjectSpawnerText": "Creator's stroke: level %1/4. Swinging the bat creates %2 objects on the field, cooldown interval is %3 seconds.", + "SkillsShadowStrikeLevelInfoText": "Level %1. The range of the strike is %2 cells up, can destroy %3 objects, cooldown interval is %4 seconds.", + "SkillsShadowStrikeName": "phantom strike", + "SkillsShadowStrikeNameFull": "Phantom strike.", + "SkillsShadowStrikeText": "Phantom strike: level %1/5. The range of the strike is %2 cells up, can destroy %3 objects, cooldown interval is %4 seconds.", + "SkillsSuperElasticityLevelInfoText": "Level %1. For %2 seconds the ball bounces off objects an order of magnitude stronger, cooldown interval is %3 seconds.", + "SkillsSuperElasticityName": "superelasticity", + "SkillsSuperElasticityNameFull": "Superelasticity.", + "SkillsSuperElasticityText": "Superelasticity: level %1/4. For %2 seconds the ball bounces off objects an order of magnitude stronger, cooldown interval is %3 seconds.", + "StatItemBallCeilHits": "Number of ball bounces from the ceiling: %1. %2", + "StatItemBallFloorHits": "Number of ball bounces off the floor: %1. %2", + "StatItemBallsCaught": "Total balls caught: %1. %2", + "StatItemBatHits": "Number of balls hit: %1. %2", + "StatItemDestroyedObjects": "Number of objects destroyed: %1. %2", + "StatItemMaximumPoints": "Maximum points per game: %1. %2", + "StatItemObjectHits": "Number of collisions with objects: %1. %2", + "StatItemPerfectHitsStreak": "Perfect strikes made in a row: %1. %2", + "StatItemStepsMade": "Number of steps made: %1. %2", + "StatItemTotalGamingTime": "Total time spent on the playing field: %1. %2", + "StatItemTotalPoints": "Total points scored: %1. %2", + "StatItemTotalSkillsUsed": "Total skills used: %1. %2", + "StatItemTotalTime": "Total time spent in the application: %1. %2", + "StatRewardAvailable": "Reward is available. Rewards received: %1.", + "StatRewardError": "The reward is not yet available.", + "StatRewardRequirements": "Until reward: %1. Rewards received: %2.", + "StatRewardsTotal": "%1 awards received in total.", + "StoreButtonReset": "Reset for %1 coins", + "StoreButtonUnlock": "Unlock for %1 coins", + "StoreButtonUnlockObjectUnlocked": "Object unlocked.", + "StoreButtonUnlockSkillUnlocked": "Skill unlocked.", + "StoreButtonUnlockUnlocked": "Feature unlocked", + "StoreButtonUpgrade": "Upgrade for %1 coins", + "StoreButtonUpgradeMaxed": "Maxed out", + "StoreBuyError1": "Error: Not enough coins.", + "StoreBuyError2": "Error: This upgrade is already at maximum level.", + "StoreBuyError3": "Error: This feature is already unlocked.", + "StoreBuyError4": "Error: Not enough achievement points.", + "StoreBuyError5": "Error: no achievement points were spent.", + "StoreBuyError6": "Error: This skill is already at maximum level.", + "StoreBuyError7": "Error: This skill is already unlocked", + "StoreBuyError8": "Error: This object is already unlocked.", + "StoreCatigories": "Categories", + "StoreCatigoriesCharacter": "Character", + "StoreCatigoriesObjects": "Objects", + "StoreCatigoriesSkills": "Skills", + "StoreCoins": "Coins: %1.", + "StoreInfoTitle": "Reference Information:", + "StoreInfoTitleWithHint": "Reference Information. Use the up and down arrows to move. To exit the menu, press escape or enter at any item.", + "StoreItemBallCatchFeatureInfo": "Ball catch feature.\r\nTo try to catch the ball, press left or right control during the game.\r\nThe ball can only be caught if it is close to you.\r\nIf the attempt is successful, the sound of the ball will disappear from the playing field, and you will be able to throw it again.\r\nThis mechanic can be used in your scoring strategies or just for fun, but it is initially useful when used in conjunction with certain skills.", + "StoreItemBallCatchFeatureText": "Unlock the ball catch feature", + "StoreItemJumpFeatureInfo": "Jump feature.\r\nTo jump, press the up arrow or the w key during the game.\r\nJumping is useful in scenarios where you want to hit a ball that hasn't yet bounced off the floor.\r\nOr when you missed a good moment to strike, but still want to hit the ball.\r\nIn this case, all you need to do is jump and swing the bat at the right moment.\r\nIt is also easier to catch the ball while jumping if you have this feature unlocked.", + "StoreItemJumpFeatureText": "Unlock the jump feature", + "StoreItemMineObjectInfo": "Mine object.\r\nThe ball's collision with this object causes an explosion that violently pushes the ball away and destroys nearby objects.\r\nIf a mine's explosion hits another mine, it will explode, thereby creating a chain of explosions.", + "StoreItemMineObjectText": "Unlock the mine object.", + "StoreItemMinObjectIncreaseInfo": "Increasing the minimum number of objects on the field.\r\nInitially, the minimum number of objects is twenty-five.\r\nWhen there are fewer objects on the field than this value, as many new objects are added as are not enough to equal this number.\r\nEach upgrade level increases this value by one.", + "StoreItemMinObjectIncreaseText": "minimum number of objects on the field increase: level %1/10.", + "StoreItemPowerRecoveringInfo": "Increased blow force recovery rate.\r\nThe upgrade significantly increases the speed at which the force of a blow is restored after each swing of the bat.", + "StoreItemPowerRecoveringText": "Increased blow force recovery rate: level %1/10", + "StoreItemResetAchievementPointsInfo": "Resetting spent achievement points.\r\nWhen reset, the levels of auras and skills are reset to zero, and all achievement points spent on upgrading them are returned to you.", + "StoreItemResetAchievementPointsQuestion": "Do you really want to reset the distribution of achievement points for a hundred coins?", + "StoreItemResetAchievementPointsText": "Reset spent achievement points.", + "StoreItemSkillBallLeapInfo": "Furious leap skill.\r\nThe character makes a rapid dash towards the ball, which is close to the ground.\r\nUsage:\r\nSelect the slot in which this skill is installed.\r\nWait for the right moment to use.\r\nThen swing the bat, the character will do the rest on his own.\r\nIf the ball is out of range, the skill will fail and will go on cooldown.", + "StoreItemSkillBallLeapText": "Furious leap skill.", + "StoreItemSkillImmaterialityInfo": "Immateriality skill.\r\nWhen a ball is succesfully hit with a bat, it imposes an immateriality effect on the ball, which allows it not to bounce off objects, but to destroy them with one hit and continue flying along the same trajectory.\r\nUsage:\r\nSelect the slot in which this skill is installed.\r\nThen hit the ball with the bat.\r\nIf the effect is applied successfully, you will hear the sound of the effect being applied.\r\nSwinging the bat without hitting the ball will cause the skill to fail and it will go on cooldown.", + "StoreItemSkillImmaterialityText": "Immateriality skill.", + "StoreItemSkillMagnetismInfo": "Magnitism skill.\r\nWhen affected by this skill, the ball is attracted to nearby objects on the playing field.\r\nUsage:\r\nCatch the ball.\r\nthen select the slot in which this skill is installed.\r\nThrow the ball back onto the playing field.\r\nThe effect will be applied immediately after the throw.", + "StoreItemSkillMagnetismText": "Magnetism skill.", + "StoreItemSkillObjectSpawnerInfo": "Creator's stroke skill.\r\nWhen you swing the bat, objects are spawned on the playing field.\r\nUsage:\r\nSelect the slot in which this skill is installed.\r\nThen swing the bat.\r\nThe skill will only work if the force of the blow is restored to maximum.\r\nOtherwise, it will go on cooldown and objects will not be generated.", + "StoreItemSkillObjectSpawnerText": "Creator's stroke skill.", + "StoreItemSkillShadowStrikeInfo": "Phantom Strike skill.\r\nSwinging the bat sends out a phantom strike that only travels upward and a certain distance, knocking down a certain number of objects in the process.\r\nUsage:\r\nSelect the slot in which this skill is installed.\r\nThen move the character to the desired place on the field and swing the bat.\r\nThe skill will succeed if the blow force has been restored to its maximum.\r\nOtherwise, it will fail and go on cooldown.", + "StoreItemSkillShadowStrikeText": "Phantom Strike skill.", + "StoreItemSkillSuperElasticityInfo": "Superelasticity skill.\r\nThe ball becomes extremely elastic, which allows it to bounce off surfaces an order of magnitude stronger.\r\nUsage:\r\nSelect the slot in which this skill is installed.\r\nThen hit the ball with the bat.\r\nIf the effect is applied successfully, you will hear the sound of the effect being applied.\r\nSwinging the bat without hitting the ball will cause the skill to fail and it will go on cooldown.", + "StoreItemSkillSuperElasticityText": "Superelasticity skill.", + "StoreTitle": "Welcome to the shop.", + "StoreTitleWithHint": "Welcome to the shop. Use tab and shift+tab to move forwards and backwards on the screen. Use the up and down arrows to move through the list. To activate the selected item, press enter or spacebar.", + "TimeString1": "%1 d, %2 h, %3 min, %4 sec", + "TimeString2": "%1 h, %2 min, %3 sec", + "TimeString3": "%1 min, %2 sec", + "TimeString4": "%1 sec", + "TimeStringIncorrect": "N/A", + "UpdateAvailableQuestionText": "A new version of the game is available: %1. Do you want to update now?", + "UpdateCheckingError": "An error occurred while checking for updates.", + "UpdateProcessScreenCanceled": "Cancelling...", + "UpdateProcessScreenDownloading": "Downloading %1%: %2 / %3.", + "UpdateProcessScreenFinished": "Installing...", + "UpdateProcessScreenFinishedWithError": "An error occurred during the update. Please try again later or download a new version from the site yourself. Press enter to continue.", + "UpdateProcessScreenStarting": "Preparing to download. Please wait.", + "UpdateProcessScreenTitle": "The update is downloading. Please wait.", + "UpdateProcessScreenTitleWithHint": "The update is downloading. Please wait. To check the progress, press any arrow key.", + "WelcomeMenu1": "Welcome to ball bouncer. Press the up and down arrows to navigate through this menu.", + "WelcomeMenu2": "We, the developers of this game, are glad that you downloaded this game!", + "WelcomeMenu3": "To get started, we strongly recommend that you read the help documentation for this game, which is located in %1.", + "WelcomeMenu4": "Or press enter here to open the documentation.", + "WelcomeMenu5": "Have a great time, player.", + "WelcomeMenu6": "Press enter to continue.", + "YesOrNoScreenNo": "No", + "YesOrNoScreenYes": "Yes" +} \ No newline at end of file diff --git a/BallBouncer/russian.lng b/BallBouncer/russian.lng new file mode 100644 index 0000000..227df7d --- /dev/null +++ b/BallBouncer/russian.lng @@ -0,0 +1,217 @@ +{ + "AchievementPoints": "Очки достижений: %1.", + "AchievementPointsTotal": "Всего получено %1 очков достижений.", + "AuraLeaderText": "Аура лидера: уровень %1. За каждый уровень прибавляет один процент к получаемым очкам.", + "AuraTimeText": "Аура времени: уровень %1. За каждый уровень прибавляет 5 секунд к максимальному игровому времени.", + "AuraUpgradeButton": "Улучшить за %1 очков достижений", + "BallLoopSelectionMenuSound": "Звук %1.", + "BallLoopSelectionMenuTitle": "Выберите звук мяча.", + "BallLoopSelectionMenuTitleWithHint": "Выберите звук мяча. Для перемещения используйте стрелки вверх и вниз. Чтобы прослушать звук, нажмите пробел. Для подтверждения выбора нажмите клавишу enter.", + "CheckUpdatesScreenChecking": "Проверка...", + "CheckUpdatesScreenNoUpdates": "У вас самая последняя версия.", + "Culture": "ru", + "FileSizeBytes": "%1 б", + "FileSizeGigabytes": "%1 гб", + "FileSizeKilobytes": "%1 кб", + "FileSizeMegabytes": "%1 мб", + "GamePoints": "%1 очков набрано в этой игре.", + "GameResultScreenCoins": "Получено %1 монет.", + "GameResultScreenPoints": "Набрано %1 очков.", + "GameResultScreenTitle": "Результаты прошедшей игры.", + "GameResultScreenTitleWithHint": "Результаты прошедшей игры. Для перемещения по меню используйте стрелки вниз и вверх. Чтобы закрыть меню, на любом пункте нажмите enter.", + "GameRewardAvailableDialog": "Вам доступна награда! Получите её в профиле на вкладке статистики!", + "GameTimeToEnd": "%1 секунд осталось до окончания игры.", + "IncompatableSaveMessage": "Загруженное сохранение принадлежит более новой версии игры\r\nи содержит данные, которые будут потеряны.\r\nВ связи с этим, сохранение считается несовместимым.\r\nИгра будет закрыта.", + "IncompatableSaveTitle": "Несовместимое сохранение", + "Language": "russian", + "LearnSoundsBatHitSuccess": "Успешный удар битой по мячу.", + "LearnSoundsCeilBounce": "Мяч отскочил от потолка.", + "LearnSoundsFineStanding": "Оповещение о начале начисления штрафа за долгое нахождение персонажа на одном месте.", + "LearnSoundsFineSwinging": "Оповещение о штрафе за частые промахи битой по мячу.", + "LearnSoundsForceFull": "Оповещение о полном восстановлении силы удара персонажа.", + "LearnSoundsObjectDestroy": "Оповещение о уничтожении объекта.", + "LearnSoundsObjectHit": "Оповещение о столкновении мяча с объектом.", + "LearnSoundsPerfectHit": "Проведён идеальный удар.", + "LearnSoundsScoresUnderNeg500": "Очки опустились ниже минус пятисот.", + "LearnSoundsTitle": "Меню изучения звуков.", + "LearnSoundsTitleWithHint": "Меню изучения звуков. Для перемещения используйте стрелки вверх и вниз. Для прослушивания звука нажмите enter. Для выхода из меню нажмите соответствующий пункт меню либо клавишу escape.", + "MainMenuExitGame": "Выход из игры.", + "MainMenuOptions": "Настройки.", + "MainMenuProfile": "Профиль.", + "MainMenuStartGame": "Начать игру.", + "MainMenuStore": "Магазин.", + "MainMenuTitle": "Главное меню.", + "MainMenuTitleWithHint": "Главное меню. Для перемещения используйте стрелки вверх и вниз. Для выбора пункта меню нажмите enter.", + "ModeSelectionMenuBack": "Вернуться в главное меню.", + "ModeSelectionMenuNormalMode": "Обычная игра.", + "ModeSelectionMenuPracticeMode": "Режим тренировки.", + "ModeSelectionMenuTitle": "Выберите режим игры.", + "ModeSelectionMenuTitleWithHint": "Выберите режим игры. Для перемещения используйте стрелки вверх и вниз. Для подтверждения выбора нажмите клавишу enter.", + "NumberNotationsEmpty": "Без нотации.", + "NumberNotationsEngineering": "Инженерная.", + "NumberNotationsLetter": "Буквенная.", + "NumberNotationsScientific": "Научная.", + "NumberNotationsStandard": "Стандартная.", + "OptionsScreenAllowOverMaxVolume": "Разрешить усиление громкости звука до двухсот процентов", + "OptionsScreenAllowPlayObjectNotifySounds": "Проигрывать звуки оповещения при столкновении и разрушении мячом объектов на поле", + "OptionsScreenBatHitSuccessSound": "Проигрывать звук при успешном ударе битой мяча", + "OptionsScreenCatigoriesBehaviour": "Поведение", + "OptionsScreenCatigoriesGeneral": "Общие", + "OptionsScreenCatigoriesSound": "Звук", + "OptionsScreenChangeBallLoopSoundButton": "Изменить звук мяча", + "OptionsScreenChangeLanguageButton": "Сменить язык", + "OptionsScreenCheckForUpdates": "Проверить обновления", + "OptionsScreenCheckForUpdatesOnStartup": "Проверять наличие обновлений при запуске", + "OptionsScreenCloseButton": "Закрыть", + "OptionsScreenDisplayTitleHints": "Показывать подсказки навигации в заголовках меню", + "OptionsScreenGameVolumeSlider": "Громкость игры", + "OptionsScreenLearnSounds": "Изучить звуки игры", + "OptionsScreenMusicVolumeSlider": "Громкость музыки", + "OptionsScreenNumberNotationSelector": "Нотация для вывода чисел", + "OptionsScreenPauseGameWhenWindowLostsFocus": "Ставить игру на паузу при переключении в другое окно", + "OptionsScreenPrefereSapi": "Предпочитать sapi средству чтения с экрана", + "OptionsScreenShowResultMenu": "Показать меню результатов в конце игры", + "OptionsScreenShowRewardAvailableDialog": "Показать уведомление о доступной награде в конце игры", + "OptionsScreenShowUpdateCheckErrorOnStartup": "Показывать ошибку если не удалось проверить наличие обновлений при запуске игры", + "OptionsScreenSoundPanRepresentationMode": "Режим панорамирования звука", + "OptionsScreenSoundPanRepresentationModeNarrowedCenter": "Суженный центр", + "OptionsScreenSoundPanRepresentationModeNormal": "Обычный", + "OptionsScreenTitle": "Экран настроек.", + "OptionsScreenTitleWithHint": "Экран настроек. Для перемещения вперёд по экрану используйте клавишу tab, назад - shift плюс tab. Для изменения положения ползунков используйте клавиши стрелки. Для активации выбранного пункта нажмите enter или пробел.", + "PauseMenuAbortGame": "Прервать игру и вернуться в главное меню.", + "PauseMenuContinueGame": "Продолжить игру.", + "PauseMenuGameInfo": "В этой игре набрано %1 очков. Мяч столкнулся с объектами %2 раз, разрушил %3 объектов, %4 раз отскочил от пола. Вы ударили по мячу %5 раз.", + "PauseMenuPracticeModeActive": "Активен режим тренировки.", + "PauseMenuTimeToEnd": "Игра закончится через %1 секунд.", + "PauseMenuTitle": "Меню паузы.", + "PauseMenuTitleWithHint": "Меню паузы. Для перемещения используйте стрелки вверх и вниз. Для активации выбранного пункта нажмите enter.", + "ProfileAurasPageTitle": "Ауры", + "ProfileSkillsPageTitle": "Умения", + "ProfileStatsPageTitle": "Статистика", + "ProfileTitle": "Ваш профиль.", + "ProfileTitleWithHint": "Ваш профиль. Используйте клавиши tab для перемещения вперёд, shift+tab - назад. Для получения награды в статистике, нажмите enter на пункте с доступной наградой.", + "SkillsBallLeapLevelInfoText": "Уровень %1. Персонаж совершает стремительный рывок к мячу, находящемуся не выше %2 клеток над землёй, Промежуток между применениями %3 секунд.", + "SkillsBallLeapName": "яростный рывок", + "SkillsBallLeapNameFull": "Яростный рывок.", + "SkillsBallLeapText": "Яростный рывок: уровень %1/4. Персонаж совершает стремительный рывок к мячу, находящемуся не выше %2 клеток над землёй, Промежуток между применениями %3 секунд.", + "SkillsEmpty": "Вы ещё не разблокировали ни одного умения.", + "SkillsImmaterialityLevelInfoText": "Уровень %1. Мяч уходит в нематериальное состояние на %2 секунд, промежуток между применениями %3 секунд.", + "SkillsImmaterialityName": "нематериальность", + "SkillsImmaterialityNameFull": "Нематериальность.", + "SkillsImmaterialityText": "Нематериальность: уровень %1/3. Мяч уходит в нематериальное состояние на %2 секунд, промежуток между применениями %3 секунд.", + "SkillsLevelInfo": "Список эффектов умения по уровню.", + "SkillsListTitle": "Разблокированные умения", + "SkillSlotRecharging": "%1 секунд до перезарядки умения.", + "SkillSlotSelectCanceled": "Выбор отменён.", + "SkillSlotSelected": "Умение %1 выбрано.", + "SkillSlotSetterMenuBack": "Вернуться назад.", + "SkillSlotSetterMenuTitle": "Выберите умение для установки в слот.", + "SkillSlotSetterMenuTitleWithHint": "Выберите умение для установки в слот. Для выбора используйте стрелки вверх и вниз, для подтверждения выбора нажмите enter.", + "SkillSlotsTitle": "Слоты", + "SkillSlotText": "Слот %1. %2", + "SkillsMagnetismLevelInfoText": "Уровень %1. Мяч на %2 секунд получает эффект, притягивающий его к ближайшим объектам, промежуток между применениями %3 секунд.", + "SkillsMagnetismName": "магнетизм", + "SkillsMagnetismNameFull": "Магнетизм.", + "SkillsMagnetismText": "Магнетизм: уровень %1/4. Мяч на %2 секунд получает эффект, притягивающий его к ближайшим объектам, промежуток между применениями %3 секунд.", + "SkillsNoneNameFull": "Не установлено.", + "SkillsObjectSpawnerLevelInfoText": "Уровень %1. Взмах битой создаёт %2 объектов на поле, промежуток между применениями %3 секунд.", + "SkillsObjectSpawnerName": "взмах творца", + "SkillsObjectSpawnerNameFull": "Взмах творца.", + "SkillsObjectSpawnerText": "Взмах творца: уровень %1/4. Взмах битой создаёт %2 объектов на поле, промежуток между применениями %3 секунд.", + "SkillsShadowStrikeLevelInfoText": "Уровень %1. Дальность полёта удара %2 клеток вверх, может уничтожить %3 объектов, промежуток между применениями %4 секунд.", + "SkillsShadowStrikeName": "призрачный удар", + "SkillsShadowStrikeNameFull": "Призрачный удар.", + "SkillsShadowStrikeText": "Призрачный удар: уровень %1/5. Дальность полёта удара %2 клеток вверх, может уничтожить %3 объектов, промежуток между применениями %4 секунд.", + "SkillsSuperElasticityLevelInfoText": "Уровень %1. На %2 секунд мяч на порядок сильнее отскакивает от объектов, промежуток между применениями %3 секунд.", + "SkillsSuperElasticityName": "сверхупругость", + "SkillsSuperElasticityNameFull": "Сверхупругость.", + "SkillsSuperElasticityText": "Сверхупругость: уровень %1/4. На %2 секунд мяч на порядок сильнее отскакивает от объектов, промежуток между применениями %3 секунд.", + "StatItemBallCeilHits": "Количество отскоков мяча от потолка: %1. %2", + "StatItemBallFloorHits": "Количество отскоков мяча от пола: %1. %2", + "StatItemBallsCaught": "Всего поймано мячей: %1. %2", + "StatItemBatHits": "Количество отбитых мячей: %1. %2", + "StatItemDestroyedObjects": "Количество уничтоженных объектов: %1. %2", + "StatItemMaximumPoints": "Максимальные очки за одну игру: %1. %2", + "StatItemObjectHits": "Количество столкновений с объектами: %1. %2", + "StatItemPerfectHitsStreak": "Совершено идеальных отбитий подряд: %1. %2", + "StatItemStepsMade": "Количество сделанных шагов: %1. %2", + "StatItemTotalGamingTime": "Всего проведено времени на игровом поле: %1. %2", + "StatItemTotalPoints": "Всего очков набрано: %1. %2", + "StatItemTotalSkillsUsed": "Всего использовано умений: %1. %2", + "StatItemTotalTime": "Всего проведено времени в приложении: %1. %2", + "StatRewardAvailable": "Награда доступна. Получено наград: %1.", + "StatRewardError": "Награда ещё недоступна.", + "StatRewardRequirements": "До награды: %1. Получено наград: %2.", + "StatRewardsTotal": "Всего получено %1 наград.", + "StoreButtonReset": "Сбросить за %1 монет", + "StoreButtonUnlock": "Разблокировать за %1 монет", + "StoreButtonUnlockObjectUnlocked": "Объект разблокирован.", + "StoreButtonUnlockSkillUnlocked": "Умение разблокировано.", + "StoreButtonUnlockUnlocked": "Функция разблокирована", + "StoreButtonUpgrade": "Улучшить за %1 монет", + "StoreButtonUpgradeMaxed": "Улучшено до максимума", + "StoreBuyError1": "Ошибка: недостаточно монет.", + "StoreBuyError2": "Ошибка: это улучшение уже имеет максимальный уровень.", + "StoreBuyError3": "Ошибка: эта функция уже разблокирована.", + "StoreBuyError4": "Ошибка: недостаточно очков достижений.", + "StoreBuyError5": "Ошибка: непотрачено ни одного очка достижений.", + "StoreBuyError6": "Ошибка: Это умение уже имеет максимальный уровень.", + "StoreBuyError7": "Ошибка: это умение уже разблокировано", + "StoreBuyError8": "Ошибка: этот объект уже разблокирован.", + "StoreCatigories": "Категории", + "StoreCatigoriesCharacter": "Персонаж", + "StoreCatigoriesObjects": "Объекты", + "StoreCatigoriesSkills": "Умения", + "StoreCoins": "Монеты: %1.", + "StoreInfoTitle": "Справочная информация: ", + "StoreInfoTitleWithHint": "Справочная информация. Для перемещения используйте стрелки вверх и вниз. Для выхода из меню нажмите escape или enter на любом пункте.", + "StoreItemBallCatchFeatureInfo": "Возможность поймать мяч.\r\nЧтобы попытаться поймать мячь, во время игры нажмите левый или правый control.\r\nМяч можно поймать только в том случае, если он находится близко к вам.\r\nЕсли попытка была успешной, звук мяча исчезнет с игрового поля, а вы снова сможете бросить его.\r\nЭту механику можно использовать в своих стратегиях по набору очков, или просто для развлечения, но изначально она полезна при применении в связке с некоторыми умениями.", + "StoreItemBallCatchFeatureText": "Разблокировка возможности поймать мяч", + "StoreItemJumpFeatureInfo": "Возможность совершать прыжок.\r\nЧтобы подпрыгнуть, во время игры нажмите стреллку вверх, либо же клавишу w.\r\nПрыжок полезен в тех сценариях, когда вы хотите отбить мяч, ещё не отскочивший от пола.\r\nЛибо же когда вы упустили удачный момент для удара, но всё равно хотите отбить мячь.\r\nВ таком случае, вам достаточно совершить прыжок и в нужный момент ударить битой.\r\nТак же в прыжке легче поймать мяч, если у вас разблокирована данная возможность.", + "StoreItemJumpFeatureText": "Разблокировка возможности совершать прыжок", + "StoreItemMineObjectInfo": "Объект мина.\r\nСтолкновение мяча с этим объектом вызывает взрыв, сильно отталкивающий мяч и уничтожающий ближайшие объекты.\r\nЕсли взрыв мины заденет другую мину, то она взорвётся, тем самым создавая цепь взрывов.", + "StoreItemMineObjectText": "Разблокировка объекта мины.", + "StoreItemMinObjectIncreaseInfo": "Увеличение минимального количества объектов на поле.\r\nИзначально минимальное количество объектов равняется двадцати пяти.\r\nКогда на поле находится объектов меньше, чем это значение, добавляются столько новых объектов, сколько не хватает, чтобы сравняться с этим числом.\r\nКаждый уровень улучшения увеличивает это значение на один.", + "StoreItemMinObjectIncreaseText": "Увеличение минимального количества объектов на поле: уровень %1/10.", + "StoreItemPowerRecoveringInfo": "Повышенная скорость восстановления силы удара.\r\nУлучшение заметно повышает скорость восстановления силы удара после каждого взмаха битой.", + "StoreItemPowerRecoveringText": "Повышенная скорость восстановления силы удара: уровень %1/10", + "StoreItemResetAchievementPointsInfo": "Сброс потраченных очков достижений.\r\nПри сбросе обнуляются уровни аур и умений, а вам возвращаются все потраченные на их улучшение очки достижений.", + "StoreItemResetAchievementPointsQuestion": "Вы действительно хотите сбросить распределение очков достижений за сто монет?", + "StoreItemResetAchievementPointsText": "Сброс потраченных очков достижений.", + "StoreItemSkillBallLeapInfo": "Умение яростный рывок.\r\nПерсонаж совершает стремительный рывок к мячу, находящемуся близко к земле.\r\nИспользование:\r\nВыберите слот, в который установлено данное умение.\r\nДождитесь удачного момента для применения.\r\nЗатем сделайте взмах битой, остальное персонаж совершит самостоятельно.\r\nЕсли мяч находится вне зоны действия, выполнение умения будет провалено и оно уйдёт на перезарядку.", + "StoreItemSkillBallLeapText": "Умение яростный рывок.", + "StoreItemSkillImmaterialityInfo": "Умение нематериальность.\r\nПри отбитии битой накладывает на мяч эффект нематериальности, который позволяет ему не отскакивать от объектов, а уничтожать их одним ударом и продолжать полёт по прежней траектории.\r\nИспользование:\r\nВыберите слот, в который установлено данное умение.\r\nЗатем отбейте мяч битой.\r\nЕсли наложение эффекта произошло успешно, вы услышите звук наложения данного эффекта.\r\nВзмах битой без отбития мяча приведёт к тому, что выполнение умения будет провалено и оно уйдёт на перезарядку.", + "StoreItemSkillImmaterialityText": "Умение нематериальность.", + "StoreItemSkillMagnetismInfo": "Умение магнетизм.\r\nНакладывает на мяч эффект, который притягивает его к ближайшим объектам на игровом поле.\r\nИспользование:\r\nПоймайте мяч.\r\nзатем выберите слот, в который установлено данное умение.\r\nБросьте мяч обратно на игровое поле.\r\nЭффект наложится сразу же после броска.", + "StoreItemSkillMagnetismText": "Умение магнетизм.", + "StoreItemSkillObjectSpawnerInfo": "Умение взмах творца.\r\nПри взмахе битой генерирует объекты на игровом поле.\r\nИспользование:\r\nВыберите слот, в который установлено данное умение.\r\nЗатем совершите взмах битой.\r\nУмение сработает только если сила удара восстановлена до максимума.\r\nВ противном случае оно уйдёт на перезарядку, а объекты не будут сгенерированы.", + "StoreItemSkillObjectSpawnerText": "Умение взмах творца.", + "StoreItemSkillShadowStrikeInfo": "Умение призрачный удар.\r\nПри взмахе битой посылается призрачный удар, который летит только вверх и на определённую дальность, и который сбивает определённое количество объектов.\r\nИспользование:\r\nВыберите слот, в который установлено данное умение.\r\nЗатем переместите персонажа в нужное место на поле и совершите взмах битой.\r\nУдар можно послать только если сила удара восстановлена до максимума.\r\nВ противном случае умение уйдёт на перезарядку, а удар не будет создан.", + "StoreItemSkillShadowStrikeText": "Умение призрачный удар.", + "StoreItemSkillSuperElasticityInfo": "Умение сверхупругость.\r\nМяч становится крайне упругим, что позволяет ему отскакивать от поверхностей на порядок сильнее.\r\nИспользование:\r\nВыберите слот, в который установлено данное умение.\r\nЗатем отбейте мяч битой.\r\nЕсли наложение эффекта произошло успешно, вы услышите звук наложения данного эффекта.\r\nВзмах битой без отбития мяча приведёт к тому, что выполнение умения будет провалено и оно уйдёт на перезарядку.", + "StoreItemSkillSuperElasticityText": "Умение сверхупругость.", + "StoreTitle": "Добро пожаловать в магазин.", + "StoreTitleWithHint": "Добро пожаловать в магазин. Для перемещения вперёд по экрану используйте клавишу tab, назад - shift плюс tab. Для перемещения по списку используйте стрелки вверх и вниз. Для активации выбранного пункта нажмите enter или пробел.", + "TimeString1": "%1 дн, %2 ч, %3 мин, %4 сек", + "TimeString2": "%1 ч, %2 мин, %3 сек", + "TimeString3": "%1 мин, %2 сек", + "TimeString4": "%1 сек", + "TimeStringIncorrect": "N/A", + "UpdateAvailableQuestionText": "Доступна новая версия игры: %1. Хотите обновить сейчас?", + "UpdateCheckingError": "Во время проверки доступных обновлений произошла ошибка.", + "UpdateProcessScreenCanceled": "Отмена...", + "UpdateProcessScreenDownloading": "Скачивание %1%: %2 / %3.", + "UpdateProcessScreenFinished": "Установка...", + "UpdateProcessScreenFinishedWithError": "Во время обновления произошла ошибка. Пожалуйста, попробуйте позже или самостоятельно скачайте новую версию с сайта. Нажмите enter чтобы продолжить.", + "UpdateProcessScreenStarting": "Подготовка к скачиванию. Пожалуйста, подождите.", + "UpdateProcessScreenTitle": "Обновление скачивается. Пожалуйста, подождите.", + "UpdateProcessScreenTitleWithHint": "Обновление скачивается. Пожалуйста, подождите. Чтобы узнать прогресс, нажмите любую клавишу стрелку.", + "WelcomeMenu1": "Добро пожаловать в игру ball bouncer. Нажимайте стрелки вверх и вниз для перемещения по данному меню.", + "WelcomeMenu2": "Мы, разработчики данной игры, рады, что вы скачали эту игру!", + "WelcomeMenu3": "Для начала настоятельно рекомендуем ознакомиться со справочной документацией к этой игре, которая находится в %1.", + "WelcomeMenu4": "Или нажмите enter здесь, чтобы открыть документацию.", + "WelcomeMenu5": "Удачного времяпрепровождения, игрок.", + "WelcomeMenu6": "Для продолжения нажмите enter.", + "YesOrNoScreenNo": "Нет", + "YesOrNoScreenYes": "Да" +} \ No newline at end of file diff --git a/README-ru.md b/README-ru.md new file mode 100644 index 0000000..b085a8a --- /dev/null +++ b/README-ru.md @@ -0,0 +1,130 @@ +Внимание! + +Это тестовый репозиторий! Пожалуйста, игнорируйте его. + +Вся информация, изложенная ниже, может измениться в любой момент. Не тратьте своё время на её изучение, дождитесь официального открытия. + +# Введение +Это официальный репозиторий Sooslandia games, в котором хранятся переводы игр и вспомогательных компонентов. + +## Структура +В репозитории находятся директории проектов, такие как BallBouncer или Agsharp, эти директории содержат все файлы переводов, относящиеся к этому проекту. + +Директория scripts содержит вспомогательные скрипты, которые выполняют валидацию новых переводов. + +Файл projects.txt в корне репозитория содержит список всех проектов, он используется вспомогательными скриптами. + +## Виды файлов +### resx +В некоторых наших компонентах используются resx файлы. + +Это xml файлы ресурсов, в которых хранятся строки для конкретного языка. + +resx файлы имеют название, соответствующее названию внутренних компонентов, например AudioForms.resx + +Если код языка отсутствует в названии файла, значит он содержит строки для английского языка. + +Файл с русскими строками, например, называется AudioForms.ru.resx + +### lng +lng - это наш собственный простой формат, используемый в тех случаях, когда применение resx не требуется. + +lng-файл представляет собой json-файл, содержащий один объект. + +Внутри объекта содержатся пары ключ-значение. Ключ - идентификатор строки, значение - строка текста на конкретном языке. + +Пример: `"ModeSelectionMenuNormalMode": "Normal game."` + +Внутри объекта обязательно должен присутствовать ключ Culture, с двухбуквенным кодом языка (ISO 639-1). + +Пример: `"Culture": "en"` + +Также необходимо наличие ключа Language, содержащее название языка с маленькой буквы. + +Пример: `"Language": "english"` + +Название файла - это название языка маленькими буквами, например english.lng + +### pot +Стандартный файл шаблона перевода, генерируемый программой xgettext. + +В качестве имени файла мы используем название проекта, например Agsharp.pot + +### po +Файл перевода, совместимый с gettext. + +В качестве имени файла мы используем двухбуквенный код языка, например ru.po + +### docs +В проектах, имеющих документацию, например BallBouncer, имеется каталог docs. + +Данный каталог содержит подкаталоги, названиями которых являются двухбуквенные коды языка, например en. + +Каждый такой подкаталог содержит файлы документации проекта. + +#### md +md-файлы это файлы документации в формате markdown, например readme.md. + +#### pot +Стандартный файл шаблона перевода, генерируемый программой xgettext, такой файл генерируется для каждого md файла в docs/en, например readme.pot. + +#### po +Файл перевода, совместимый с gettext. + +## Последовательность добавления перевода +### С нашей стороны +В случае использования resx файлов, мы преобразовываем их все в lng файл с английским переводом, english.lng + +Далее из english.lng генерируется .pot файл. + +Markdown файлы английской документации обрабатываются программой mdpo, она извлекает блоки текста и формирует из них pot файлы. + +### Со стороны переводчика +Вам нужно сделать форк этого репозитория и создать ветку для нового перевода. + +Затем Вы должны выбрать предпочтительный способ перевода. + +#### po +Вы можете загрузить .pot-файл в программу для перевода, например Poedit, и выполнять перевод в ней. + +Далее готовый po-файл, например fr.po, нужно поместить в директорию проекта. + +Чтобы перевести документацию, нужно использовать в качестве шаблона pot-файлы из каталога docs/en. + +В каталоге docs следует создать подкаталог для переведённых файлов, названием должен являться двухбуквенный код языка. + +Готовые po файлы, например readme.po, нужно поместить в созданный каталог. + +#### lng +Это нерекомендованный способ, т.к. на данный момент мы не можем предложить удобного способа актуализации и валидации переводов. + +Используйте его только в том случае, если нет никакой возможности использовать poedit или аналогичное ПО. + +Вы можете переводить lng файл напрямую, в качестве шаблона можно использовать любой существующий в этом репозитории файл, но мы рекомендуем переводить english.lng. + +В случае, если вы будете использовать не english.lng, и решите отправить незавершённый перевод, обязательно удалите все непереведённые строки, чтобы файл содержал только строки на целевом языке перевода. + +Например, если вы решили переводить russian.lng на французский, перевели половину, и захотели отправить этот промежуточный результат, удалите все оставшиеся русские строки, оставив только французские. + +Это нужно для того, чтобы для непереведённых строк использовался английский вариант. + +При использовании в качестве шаблона english.lng удалять непереведённые строки не обязательно. + +В lng файле нужно задать правильные значения для ключей Culture и Language, смотрите раздел выше про формат lng. + +Далее готовый lng-файл, например french.lng, нужно добавить в директорию проекта. + +#### resx и md +Пожалуйста, не переводите resx и md файлы, мы не валидируем их, и не принимаем переводы в таком формате. + +#### Дальнейшие шаги +Когда перевод закончен и все файлы находятся на своих местах, отправляйте pull request в данный репозиторий, мы проверим его, при необходимости попросим внести изменения, и в случае корректности перевода вольём его в master. + +После чего переводы будут добавлены в соответствующие продукты и доставлены пользователям системой обновления. + +### Перевод новых / изменённых строк +Когда мы внесём изменения в наши проекты, исходные языковые файлы будут актуализированы в этом репозитории. + +Вам нужно будет смерджить ветку master в вашу ветку, а затем обновить ваш перевод из обновлённых pot файлов и перевести новые строки. + +Для lng файлов, как указано выше, мы не можем предложить удобного способа актуализирования перевода, но в будущем постараемся решить эту проблему. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae22128 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +Attention! + +This is a test repository! Please ignore it. + +All information provided below is subject to change at any time. Do not spend your time studying it; wait for the official launch. + +# Introduction +This is the official repository of Sooslandia games, where translations of games and auxiliary components are stored. + +## Structure +The repository contains project directories such as BallBouncer or Agsharp. These directories contain all translation files related to that project. + +The scripts directory contains auxiliary scripts that perform validation of new translations. + +The projects.txt file in the root of the repository contains a list of all projects and is used by auxiliary scripts. + +## Types of Files +### resx +Some of our components use resx files. + +These are XML resource files that store strings for a specific language. + +Resx files are named according to the internal components' names, such as AudioForms.resx. + +If the language code is absent in the file name, it means it contains strings for the English language. + +A file with Russian strings, for example, is named AudioForms.ru.resx. + +### lng +lng is our own simple format used in cases where resx is not required. + +An lng file is a JSON file containing a single object. + +Inside the object, there are key-value pairs. The key is the string identifier, and the value is the text string in a specific language. + +Example: `"ModeSelectionMenuNormalMode": "Normal game."` + +The object must contain the key Culture with a two-letter language code (ISO 639-1). + +Example: `"Culture": "en"` + +There must also be a Language key containing the name of the language in lowercase. + +Example: `"Language": "english"` + +The file name is the language name in lowercase, for example, english.lng. + +### pot +A standard translation template file generated by the xgettext program. + +We use the project name as the file name, for example, Agsharp.pot. + +### po +A translation file compatible with gettext. + +We use the two-letter language code as the file name, for example, ru.po. + +### docs +Projects that have documentation, such as BallBouncer, have a docs directory. + +This directory contains subdirectories named as two-letter language codes, for example en. + +Each such subdirectory contains project documentation files. + +#### md +md files are documentation files in markdown format, such as readme.md. + +#### pot +Standard translation template file generated by the xgettext program, such a file is generated for each md file in docs/en, for example readme.pot. + +#### po +A translation file compatible with gettext. + +## Sequence of Adding a Translation +### From our side +If resx files are used, we convert them all into an lng file with an English translation, english.lng. + +Then the .pot file is generated from english.lng. + +English documentation markdown files are processed by the mdpo program, it extracts blocks of text and forms pot files from them. + +### From the translator's side +You need to fork this repository and create a branch for the new translation. + +Then you should choose your preferred translation method. + +#### po +You can load the .pot file into a translation program, such as Poedit, and perform the translation there. + +Then the completed po file, for example, fr.po, needs to be added to the project directory. + +To translate documentation, you need to use pot files from the docs/en directory as a template. + +In the docs directory, you should create a subdirectory for translated files, the name should be the two-letter language code. + +Ready po files, for example readme.po, need to be placed in the created directory. + +#### lng +This is not a recommended method, because... At the moment, we cannot offer a convenient way to update and validate translations. + +Use it only if it is impossible to use poedit or similar software. + +You can translate the lng file directly. As a template, you can use any existing file in this repository, but we recommend to translate english.lng. + +If you use a file other than english.lng and decide to send an unfinished translation, be sure to delete all untranslated strings so that the file contains only strings in the target translation language. + +For example, if you decided to translate russian.lng to French, translated half of it, and decided to send this intermediate result, delete all remaining Russian strings, leaving only the French ones. + +This is necessary so that the untranslated strings fallback to the english language. + +When using english.lng as a template, it is not necessary to delete untranslated strings. + +In the lng file, you need to set the correct values for the Culture and Language keys; see the section above about the lng format. + +Then the completed lng file, for example, french.lng, needs to be added to the project directory. + +#### resx and md +Please do not translate resx and md files; we do not validate them and do not accept translations in this format. + +#### Next Steps +When the translation is completed and all files are in their places, submit a pull request with your translation. We will review it, ask for changes if necessary, and if the translation is correct, we will merge it into master. + +After that, the translations will be added to the corresponding products and delivered to users via the update system. + +### Translating new/changed strings +When we make changes to our projects, the source language files will be updated in this repository. + +You will need to merge the master branch into your branch, and then update your translation from the updated pot files and translate the new lines. + +For lng files, as stated above, we cannot offer a convenient way to update the translation, but we will try to solve this problem in the future. diff --git a/languages.json b/languages.json new file mode 100644 index 0000000..d9eae6a --- /dev/null +++ b/languages.json @@ -0,0 +1,738 @@ +[ + [ + "aa", + "Afar" + ], + [ + "ab", + "Abkhazian" + ], + [ + "ae", + "Avestan" + ], + [ + "af", + "Afrikaans" + ], + [ + "ak", + "Akan" + ], + [ + "am", + "Amharic" + ], + [ + "an", + "Aragonese" + ], + [ + "ar", + "Arabic" + ], + [ + "as", + "Assamese" + ], + [ + "av", + "Avaric" + ], + [ + "ay", + "Aymara" + ], + [ + "az", + "Azerbaijani" + ], + [ + "ba", + "Bashkir" + ], + [ + "be", + "Belarusian" + ], + [ + "bg", + "Bulgarian" + ], + [ + "bh", + "Bihari languages" + ], + [ + "bi", + "Bislama" + ], + [ + "bm", + "Bambara" + ], + [ + "bn", + "Bengali" + ], + [ + "bo", + "Tibetan" + ], + [ + "br", + "Breton" + ], + [ + "bs", + "Bosnian" + ], + [ + "ca", + "Catalan" + ], + [ + "ce", + "Chechen" + ], + [ + "ch", + "Chamorro" + ], + [ + "co", + "Corsican" + ], + [ + "cr", + "Cree" + ], + [ + "cs", + "Czech" + ], + [ + "cu", + "Church Slavic" + ], + [ + "cv", + "Chuvash" + ], + [ + "cy", + "Welsh" + ], + [ + "da", + "Danish" + ], + [ + "de", + "German" + ], + [ + "dv", + "Divehi" + ], + [ + "dz", + "Dzongkha" + ], + [ + "ee", + "Ewe" + ], + [ + "el", + "Greek, Modern (1453-)" + ], + [ + "en", + "English" + ], + [ + "eo", + "Esperanto" + ], + [ + "es", + "Spanish" + ], + [ + "et", + "Estonian" + ], + [ + "eu", + "Basque" + ], + [ + "fa", + "Persian" + ], + [ + "ff", + "Fulah" + ], + [ + "fi", + "Finnish" + ], + [ + "fj", + "Fijian" + ], + [ + "fo", + "Faroese" + ], + [ + "fr", + "French" + ], + [ + "fy", + "Western Frisian" + ], + [ + "ga", + "Irish" + ], + [ + "gd", + "Gaelic" + ], + [ + "gl", + "Galician" + ], + [ + "gn", + "Guarani" + ], + [ + "gu", + "Gujarati" + ], + [ + "gv", + "Manx" + ], + [ + "ha", + "Hausa" + ], + [ + "he", + "Hebrew" + ], + [ + "hi", + "Hindi" + ], + [ + "ho", + "Hiri Motu" + ], + [ + "hr", + "Croatian" + ], + [ + "ht", + "Haitian" + ], + [ + "hu", + "Hungarian" + ], + [ + "hy", + "Armenian" + ], + [ + "hz", + "Herero" + ], + [ + "ia", + "Interlingua (International Auxiliary Language Association)" + ], + [ + "id", + "Indonesian" + ], + [ + "ie", + "Interlingue" + ], + [ + "ig", + "Igbo" + ], + [ + "ii", + "Sichuan Yi" + ], + [ + "ik", + "Inupiaq" + ], + [ + "io", + "Ido" + ], + [ + "is", + "Icelandic" + ], + [ + "it", + "Italian" + ], + [ + "iu", + "Inuktitut" + ], + [ + "ja", + "Japanese" + ], + [ + "jv", + "Javanese" + ], + [ + "ka", + "Georgian" + ], + [ + "kg", + "Kongo" + ], + [ + "ki", + "Kikuyu" + ], + [ + "kj", + "Kuanyama" + ], + [ + "kk", + "Kazakh" + ], + [ + "kl", + "Kalaallisut" + ], + [ + "km", + "Central Khmer" + ], + [ + "kn", + "Kannada" + ], + [ + "ko", + "Korean" + ], + [ + "kr", + "Kanuri" + ], + [ + "ks", + "Kashmiri" + ], + [ + "ku", + "Kurdish" + ], + [ + "kv", + "Komi" + ], + [ + "kw", + "Cornish" + ], + [ + "ky", + "Kirghiz" + ], + [ + "la", + "Latin" + ], + [ + "lb", + "Luxembourgish" + ], + [ + "lg", + "Ganda" + ], + [ + "li", + "Limburgan" + ], + [ + "ln", + "Lingala" + ], + [ + "lo", + "Lao" + ], + [ + "lt", + "Lithuanian" + ], + [ + "lu", + "Luba-Katanga" + ], + [ + "lv", + "Latvian" + ], + [ + "mg", + "Malagasy" + ], + [ + "mh", + "Marshallese" + ], + [ + "mi", + "Maori" + ], + [ + "mk", + "Macedonian" + ], + [ + "ml", + "Malayalam" + ], + [ + "mn", + "Mongolian" + ], + [ + "mr", + "Marathi" + ], + [ + "ms", + "Malay" + ], + [ + "mt", + "Maltese" + ], + [ + "my", + "Burmese" + ], + [ + "na", + "Nauru" + ], + [ + "nb", + "Bokmål, Norwegian" + ], + [ + "nd", + "Ndebele, North" + ], + [ + "ne", + "Nepali" + ], + [ + "ng", + "Ndonga" + ], + [ + "nl", + "Dutch" + ], + [ + "nn", + "Norwegian Nynorsk" + ], + [ + "no", + "Norwegian" + ], + [ + "nr", + "Ndebele, South" + ], + [ + "nv", + "Navajo" + ], + [ + "ny", + "Chichewa" + ], + [ + "oc", + "Occitan (post 1500)" + ], + [ + "oj", + "Ojibwa" + ], + [ + "om", + "Oromo" + ], + [ + "or", + "Oriya" + ], + [ + "os", + "Ossetian" + ], + [ + "pa", + "Panjabi" + ], + [ + "pi", + "Pali" + ], + [ + "pl", + "Polish" + ], + [ + "ps", + "Pushto" + ], + [ + "pt", + "Portuguese" + ], + [ + "qu", + "Quechua" + ], + [ + "rm", + "Romansh" + ], + [ + "rn", + "Rundi" + ], + [ + "ro", + "Romanian" + ], + [ + "ru", + "Russian" + ], + [ + "rw", + "Kinyarwanda" + ], + [ + "sa", + "Sanskrit" + ], + [ + "sc", + "Sardinian" + ], + [ + "sd", + "Sindhi" + ], + [ + "se", + "Northern Sami" + ], + [ + "sg", + "Sango" + ], + [ + "si", + "Sinhala" + ], + [ + "sk", + "Slovak" + ], + [ + "sl", + "Slovenian" + ], + [ + "sm", + "Samoan" + ], + [ + "sn", + "Shona" + ], + [ + "so", + "Somali" + ], + [ + "sq", + "Albanian" + ], + [ + "sr", + "Serbian" + ], + [ + "ss", + "Swati" + ], + [ + "st", + "Sotho, Southern" + ], + [ + "su", + "Sundanese" + ], + [ + "sv", + "Swedish" + ], + [ + "sw", + "Swahili" + ], + [ + "ta", + "Tamil" + ], + [ + "te", + "Telugu" + ], + [ + "tg", + "Tajik" + ], + [ + "th", + "Thai" + ], + [ + "ti", + "Tigrinya" + ], + [ + "tk", + "Turkmen" + ], + [ + "tl", + "Tagalog" + ], + [ + "tn", + "Tswana" + ], + [ + "to", + "Tonga (Tonga Islands)" + ], + [ + "tr", + "Turkish" + ], + [ + "ts", + "Tsonga" + ], + [ + "tt", + "Tatar" + ], + [ + "tw", + "Twi" + ], + [ + "ty", + "Tahitian" + ], + [ + "ug", + "Uighur" + ], + [ + "uk", + "Ukrainian" + ], + [ + "ur", + "Urdu" + ], + [ + "uz", + "Uzbek" + ], + [ + "ve", + "Venda" + ], + [ + "vi", + "Vietnamese" + ], + [ + "vo", + "Volapük" + ], + [ + "wa", + "Walloon" + ], + [ + "wo", + "Wolof" + ], + [ + "xh", + "Xhosa" + ], + [ + "yi", + "Yiddish" + ], + [ + "yo", + "Yoruba" + ], + [ + "za", + "Zhuang" + ], + [ + "zh", + "Chinese" + ], + [ + "zu", + "Zulu" + ] +] \ No newline at end of file diff --git a/mdpo-files/Dockerfile b/mdpo-files/Dockerfile new file mode 100644 index 0000000..b0dfaec --- /dev/null +++ b/mdpo-files/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12.4-slim +RUN apt update && \ + apt install -y git build-essential cmake wget autoconf pkg-config +WORKDIR /mdpo +COPY entrypoint.sh . +ENTRYPOINT ["./entrypoint.sh"] diff --git a/mdpo-files/build.sh b/mdpo-files/build.sh new file mode 100644 index 0000000..5b0c443 --- /dev/null +++ b/mdpo-files/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +docker build -t build-mdpo . +mkdir -p wheels +docker run --rm --name build-mdpo -v ./wheels:/wheels build-mdpo $(id -u):$(id -g) +docker rmi build-mdpo +tar czf wheels.tar.gz wheels +rm -rf wheels diff --git a/mdpo-files/entrypoint.sh b/mdpo-files/entrypoint.sh new file mode 100644 index 0000000..a949166 --- /dev/null +++ b/mdpo-files/entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e +wget https://github.com/mity/md4c/archive/refs/tags/release-0.5.2.tar.gz +tar xf release-0.5.2.tar.gz +cd md4c-release-0.5.2 +mkdir build +cd build +cmake .. +make +make install +ldconfig +cd .. + +git clone --depth=1 https://github.com/NixOS/patchelf +cd patchelf +./bootstrap.sh +./configure +make +make check +make install +cd .. + +mkdir temp-wheels +cd temp-wheels +pip wheel --no-cache-dir --wheel-dir . mdpo==2.0.1 +pip install --no-cache-dir auditwheel==6.0.0 +auditwheel repair --plat manylinux_2_34_x86_64 --wheel-dir /wheels ./pymd4c-*.whl +rm ./pymd4c-*.whl +mv *.whl /wheels/ +cd .. +chown -R $1 /wheels diff --git a/mdpo-files/wheels.tar.gz b/mdpo-files/wheels.tar.gz new file mode 100644 index 0000000..c94fc06 Binary files /dev/null and b/mdpo-files/wheels.tar.gz differ diff --git a/projects.txt b/projects.txt new file mode 100644 index 0000000..75898da --- /dev/null +++ b/projects.txt @@ -0,0 +1,2 @@ +Agsharp +BallBouncer diff --git a/scripts/constants.py b/scripts/constants.py new file mode 100644 index 0000000..30d5abb --- /dev/null +++ b/scripts/constants.py @@ -0,0 +1,11 @@ +import re + +BRACES_PLACEHOLDER_REGEX = re.compile(r"(?:(?<=[^{])|^)\{(\d+)\}") +PERCENT_PLACEHOLDER_REGEX = re.compile(r"(?:(?<=[^%])|^)%(\d+)") + +RESX_FILE_REGEX = re.compile(r"^(.*?)((?<=\.)[a-z]{2})?\.resx$") +PO_FILE_REGEX = re.compile(r"^[a-z]{2}.po$") +LNG_FILE_REGEX = re.compile(r"^[a-z]*?.lng$") +DOCS_DIR_REGEX = re.compile(r"^[a-z]{2}$") + +EMAIL_ADDRESS = "contact@sooslandia.ru" diff --git a/scripts/language_manager.py b/scripts/language_manager.py new file mode 100644 index 0000000..3afb8b7 --- /dev/null +++ b/scripts/language_manager.py @@ -0,0 +1,21 @@ +import json + + +class LanguageManager: + def initialize(self, languages_file_path): + with open(languages_file_path, "rb") as f: + self.languages = json.load(f) + + def get_language_code(self, language_name): + language_name = language_name.lower() + for code, name in self.languages: + if name.lower() == language_name: + return code + + def get_language_name(self, language_code): + for code, name in self.languages: + if code == language_code: + return name + + +language_manager = LanguageManager() diff --git a/scripts/message_manager.py b/scripts/message_manager.py new file mode 100644 index 0000000..0400a9b --- /dev/null +++ b/scripts/message_manager.py @@ -0,0 +1,20 @@ +class MessageManager: + def __init__(self): + self.messages = [] + + def add_message(self, message): + self.messages.append(message) + + def add_list_message(self, header, messages_list): + if (total_messages := len(messages_list)) > 50: + messages_list = messages_list[:50] + messages_list.append(f"{total_messages-50} more...") + self.messages.append(header) + self.messages.extend("- " + i for i in messages_list) + self.messages.append("\n") + + def get_messages(self): + return self.messages + + +message_manager = MessageManager() diff --git a/scripts/process_master_changes.py b/scripts/process_master_changes.py new file mode 100644 index 0000000..06f8f8f --- /dev/null +++ b/scripts/process_master_changes.py @@ -0,0 +1,210 @@ +import json +import logging +import os +import subprocess +from pathlib import Path +from xml.etree import ElementTree + +from constants import EMAIL_ADDRESS +from utils import ( + convert_braces_to_percents, + convert_percents_to_braces, + file_updated, + parse_resx_filename, +) + +REPOSITORY_DIR = Path(".") +PROJECTS_DIR = Path(os.environ["PROJECTS_DIR"]) + +logger = logging.getLogger("process_master_changes") +logger.setLevel(logging.DEBUG) +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.INFO) +logger.addHandler(console_handler) + + +def process_project(project_path): + logger.info(f"Processing project {project_path}") + convert_resx_files(project_path) + generate_pot_file(project_path) + process_docs(project_path) + logger.info("Project processed") + + +def convert_resx_files(project_path): + logger.info("Processing resx files") + english_lng_file = project_path / "english.lng" + english_files = [] + has_changes = False + for file in project_path.glob("*.resx"): + if parse_resx_filename(file.name)[1] is None: + english_files.append(file) + if not has_changes and file_updated(file): + has_changes = True + logger.info(f"Found {len(english_files)} english resx files") + if not has_changes and english_lng_file.exists(): + logger.info("No changed english resx files found") + return + lng = {"Culture": "en", "Language": "english"} + for file in english_files: + lng |= parse_resx(file) + lng = json.dumps(lng, indent=2, ensure_ascii=False, sort_keys=True) + with english_lng_file.open("w", encoding="utf-8", newline="") as f: + f.write(lng) + logger.info("Resx files processed") + + +def parse_resx(file): + logger.info(f"Parsing resx file {file}") + namespace, _ = parse_resx_filename(file.name) + logger.info(f"File namespace is {namespace}") + with file.open("r", encoding="utf-8") as f: + root = ElementTree.fromstring(f.read()) + lng = {} + for data in root.iterfind("data"): + name = data.attrib["name"] + text = data.find("value").text + text = convert_braces_to_percents(text) + lng[f"{namespace}_{name}"] = text + logger.info("Resx file parsed") + return lng + + +def generate_pot_file(project_path): + logger.info("Generating pot file from english lng file") + pot_file = project_path / (project_path.name + ".pot") + english_file = project_path / "english.lng" + if not file_updated(english_file) and pot_file.exists(): + logger.info("File is not changed") + return + with english_file.open("r") as f: + lng = json.load(f) + lng.pop("Culture") + source = [] + for identifier, string in lng.items(): + source.append(f"# {identifier}") + source.append(get_source_line_for_pot(convert_percents_to_braces(string))) + source = "\n".join(source) + source += "\n" + pot = generate_pot_file_from_source(source, project_path.name) + with pot_file.open("w", encoding="utf-8", newline="") as f: + f.write(pot) + logger.info("Pot file generated") + + +def get_source_line_for_pot(string): + quote = None + for q in ['"', "'", '"""', "'''"]: + if q not in string: + quote = q + break + if quote is None: + raise RuntimeError(f"Failed to find quotes for string {string[:200]}") + return f"_({quote}{string}{quote})" + + +def generate_pot_file_from_source(source, package_name): + logger.info("Generating pot file from source") + process = subprocess.Popen( + [ + "xgettext", + "-o", + "-", + "--language=Python", + "--no-location", + "--add-comments", + f"--msgid-bugs-address={EMAIL_ADDRESS}", + f"--package-name={package_name}", + "-", + ], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + process.stdin.write(source.encode()) + process.stdin.flush() + process.stdin.close() # EOF + pot = b"" + while process.poll() is None: + pot += process.stdout.read() + if process.returncode != 0: + raise RuntimeError(f"xgettext failed with code {process.returncode}") + logger.info("Pot generated from source") + return pot.decode() + + +def process_docs(project_path): + logger.info("Processing docs") + docs_path = project_path / "docs" + if not docs_path.is_dir(): + logger.info("No docs in project") + return + en_docs_path = docs_path / "en" + process_en_docs(en_docs_path) + logger.info("Docs processed") + + +def process_en_docs(en_docs_path): + logger.info(f"Processing english docs: {en_docs_path}") + for md_file in en_docs_path.glob("*.md"): + process_docs_md_file(md_file) + logger.info("English docs processed") + + +def process_docs_md_file(md_file): + logger.info(f"Processing docs md file {md_file}") + package_name = md_file.relative_to(REPOSITORY_DIR).parts[0] + pot_file = md_file.with_suffix(".pot") + if not file_updated(md_file) and pot_file.exists(): + logger.info("Md file is not changed") + return + process = subprocess.Popen( + [ + "md2po", + "-d", + "Content-Type: text/plain; charset=utf-8", + md_file, + ], + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + ) + po = b"" + while process.poll() is None: + po += process.stdout.read() + if process.returncode != 0: + raise RuntimeError(f"md2po failed with code {process.returncode}") + process = subprocess.Popen( + [ + "xgettext", + "-o", + "-", + "--language=po", + "--add-comments", + f"--msgid-bugs-address={EMAIL_ADDRESS}", + f"--package-name={package_name}", + "-", + ], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + process.stdin.write(po) + process.stdin.flush() + process.stdin.close() # EOF + pot = b"" + while process.poll() is None: + pot += process.stdout.read() + if process.returncode != 0: + raise RuntimeError(f"xgettext failed with code {process.returncode}") + with pot_file.open("wb") as f: + f.write(pot) + logger.info("Md file processed") + + +def main(): + with (REPOSITORY_DIR / "projects.txt").open("r", encoding="utf-8") as f: + project_dirs = [i for i in f.read().split("\n") if i] + for project_dir in project_dirs: + process_project(PROJECTS_DIR / project_dir) + + +if __name__ == "__main__": + main() diff --git a/scripts/process_translations.py b/scripts/process_translations.py new file mode 100644 index 0000000..55637a3 --- /dev/null +++ b/scripts/process_translations.py @@ -0,0 +1,345 @@ +import json +import logging +import os +import subprocess +from gettext import GNUTranslations +from io import BytesIO +from pathlib import Path +from xml.etree import ElementTree + +from constants import DOCS_DIR_REGEX, LNG_FILE_REGEX, PO_FILE_REGEX +from language_manager import language_manager +from message_manager import message_manager +from utils import ( + convert_braces_to_percents, + convert_percents_to_braces, + get_percent_placeholders, + parse_resx_filename, +) + +REPOSITORY_DIR = Path(".") +PROJECTS_DIR = Path(os.environ["PROJECTS_DIR"]) + +logger = logging.getLogger("process_master_changes") +logger.setLevel(logging.DEBUG) +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.INFO) +logger.addHandler(console_handler) + + +def process_project(project_path): + logger.info(f"Processing project {project_path}") + process_po_files(project_path) + process_lng_files(project_path) + generate_resx_files(project_path) + process_docs(project_path) + logger.info("Project processed") + + +def get_english_lng(project_path): + with (REPOSITORY_DIR / project_path.name / "english.lng").open( + "r", encoding="utf-8" + ) as f: + english_lng = json.load(f) + english_lng.pop("Culture") + english_lng.pop("Language") + return english_lng + + +def process_po_files(project_path): + logger.info("Processing po files") + english_lng = get_english_lng(project_path) + valid_po_files = [] + errors = [] + for file in project_path.glob("*.po"): + if not PO_FILE_REGEX.match(file.name): + errors.append(f"PO file {file} have incorrect name.") + continue + language_code = file.name.split(".")[0] + if not (language_name := language_manager.get_language_name(language_code)): + errors.append( + f"PO file {file} have incorrect name. ISO 639-1 code is unknown" + ) + continue + valid_po_files.append((file, language_code, language_name)) + if errors: + message_manager.add_list_message("Invalid PO files found", errors) + logger.info(f"Found {len(valid_po_files)} valid po files") + for file, language_code, language_name in valid_po_files: + convert_po_to_lng( + project_path=project_path, + english_lng=english_lng, + file=file, + language_code=language_code, + language_name=language_name, + ) + logger.info("Po files processed") + + +def convert_po_to_mo(file): + process = subprocess.Popen( + ["msgfmt", "-o", "-", str(file)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + mo = b"" + while process.poll() is None: + mo += process.stdout.read() + if process.returncode != 0: + raise RuntimeError(f"msgfmt failed with code {process.returncode}") + return mo + + +def convert_po_to_lng(*, project_path, english_lng, file, language_code, language_name): + logger.info(f"Converting po file {file} to lng") + lng_file = project_path / (language_name.lower() + ".lng") + mo = BytesIO(convert_po_to_mo(file)) + po_translation = GNUTranslations(mo) + missing_strings = [] + lng = {"Culture": language_code, "Language": language_name.lower()} + for identifier, string in english_lng.items(): + translated_string = po_translation._catalog.get( + convert_percents_to_braces(string), None + ) + if translated_string is None: + missing_strings.append(f"{identifier} - {string[:200]}") + continue + lng[identifier] = convert_braces_to_percents(translated_string) + if missing_strings: + message_manager.add_list_message( + f"Missing strings in {project_path.name}/{file.name} PO translation", + missing_strings, + ) + with lng_file.open("w", encoding="utf-8", newline="") as f: + json.dump(lng, f, ensure_ascii=False, indent=2, sort_keys=True) + logger.info(f"Po file converted to lng file {lng_file}')") + + +def process_lng_files(project_path): + logger.info("Processing lng files") + english_lng = get_english_lng(project_path) + valid_lng_files = [] + errors = [] + for file in project_path.glob("*.lng"): + if not LNG_FILE_REGEX.match(file.name): + errors.append(f"lng file {file} have incorrect name.") + continue + language_name = file.name.split(".")[0] + if not (language_code := language_manager.get_language_code(language_name)): + errors.append( + f"lng file {file} have incorrect name. Failed to detect ISO 639-1 code" + ) + continue + valid_lng_files.append((file, language_code, language_name)) + if errors: + message_manager.add_list_message("Invalid lng files found", errors) + for file, language_code, language_name in valid_lng_files: + process_lng_file( + project_path=project_path, + english_lng=english_lng, + file=file, + language_code=language_code, + language_name=language_name, + ) + logger.info("Lng files processed") + + +def process_lng_file(*, project_path, english_lng, file, language_code, language_name): + logger.info(f"Processing lng file {file}") + with file.open("r", encoding="utf-8") as f: + try: + lng = json.load(f) + except json.JSONDecodeError as e: + message_manager.add_message( + f"Failed to load {project_path.name}/{file.name}: {str(e)}" + ) + return + errors = [] + missing = object() + for key, required_value in [ + ("Culture", language_code), + ("Language", language_name), + ]: + lng_value = lng.get(key, missing) + if lng_value is missing: + errors.append(f"{key} key is missing") + elif lng_value != required_value: + errors.append( + f"{key} key value is incorrect. Required: {required_value}, Actual: {str(lng_value)[:200]}" + ) + missing_strings = [] + placeholder_errors = [] + for identifier, string in english_lng.items(): + if identifier not in lng: + missing_strings.append(f"{identifier} - {string[:200]}") + continue + placeholder_errors.extend( + identifier + " - " + i + for i in validate_placeholders(english_lng[identifier], lng[identifier]) + ) + if errors: + message_manager.add_list_message( + f"Errors in {project_path.name}/{file.name} lng translation", + errors, + ) + if missing_strings: + message_manager.add_list_message( + f"Missing strings in {project_path.name}/{file.name} lng translation", + missing_strings, + ) + if placeholder_errors: + message_manager.add_list_message( + f"Placeholder errors in {project_path.name}/{file.name} lng translation", + placeholder_errors, + ) + logger.info("Lng file processed") + + +def validate_placeholders(original_string, translated_string): + original_placeholders = get_percent_placeholders(original_string) + translated_placeholders = get_percent_placeholders(translated_string) + errors = [] + for p in original_placeholders: + if (count := translated_placeholders.count(p)) == 1: + continue + if count == 0: + errors.append(f"Placeholder %{p} not found") + else: + errors.append(f"Placeholder %{p} duplicated") + for p in translated_placeholders: + if p not in original_placeholders: + errors.append(f"Extra placeholder %{p} found in translated string") + return errors + + +def generate_resx_files(project_path): + logger.info("Generating resx files") + resx_files = [] + for file in project_path.glob("*.resx"): + if parse_resx_filename(file.name)[1] is None: + resx_files.append(file) + for lng_file in project_path.glob("*.lng"): + if lng_file.name == "english.lng": + continue + with lng_file.open("r", encoding="utf-8") as f: + try: + lng = json.load(f) + except json.JSONDecodeError: + # Should be handled and added to message_manager by lng validation function, + # Let's not add this error again. + continue + errors = [] + for resx_file in resx_files: + errors.extend(generate_resx_from_lng(lng, resx_file)) + if errors: + message_manager.add_list_message( + f"Errors when generating resx from {project_path.name}/{lng_file.name}", + errors, + ) + logger.info("Resx files generated") + + +def generate_resx_from_lng(lng, source_resx_file): + namespace, _ = parse_resx_filename(source_resx_file.name) + target_resx_file = source_resx_file.with_name(f'{namespace}.{lng['Culture']}.resx') + logger.info(f"Generating resx file {target_resx_file} from lng file") + with source_resx_file.open("r", encoding="utf-8") as f: + root = ElementTree.fromstring(f.read()) + errors = [] + for data in root.iterfind("data"): + name = data.attrib["name"] + value = data.find("value") + identifier = namespace + "_" + name + if identifier not in lng: + errors.append(f"Key {identifier} not found") + continue + value.text = convert_percents_to_braces(lng[identifier]) + with target_resx_file.open("wb") as f: + f.write(ElementTree.tostring(root, encoding="utf-8")) + logger.info("Resx file generated") + return errors + + +def process_docs(project_path): + logger.info("Processing docs") + docs_path = project_path / "docs" + if not docs_path.exists(): + logger.info("No docs in project") + valid_docs_dirs = [] + errors = [] + for docs_dir in docs_path.glob("*"): + if docs_dir.name == "en": + continue + if not docs_dir.is_dir(): + errors.append(f"Object must be a directory: {docs_dir}") + elif not DOCS_DIR_REGEX.match(docs_dir.name): + errors.append(f"Docs dir name is invalid: {docs_dir}") + else: + valid_docs_dirs.append(docs_dir) + if errors: + message_manager.add_list_message( + f"Invalid docs found in project {project_path.name}", errors + ) + for docs_dir in valid_docs_dirs: + process_language_docs(docs_dir) + logger.info("Docs processed") + + +def process_language_docs(docs_dir): + logger.info(f"Processing language docs {docs_dir}") + errors = [] + for po_file in docs_dir.glob("*.po"): + errors.extend(convert_docs_po_to_md_file(po_file)) + if errors: + message_manager.add_list_message(f"Docs processing errors: {docs_dir}", errors) + logger.info("Language docs processed") + + +def convert_docs_po_to_md_file(po_file): + logger.info(f"Converting docs po file {po_file} to md file") + source_md_file = po_file.parent.parent / "en" / po_file.with_suffix(".md").name + if not source_md_file.is_file(): + return [ + f"Failed to find source file for {po_file}. File {source_md_file} not found." + ] + process = subprocess.Popen( + [ + "po2md", + "-q", + "-p", + po_file, + "-s", + po_file.with_suffix(".md"), + source_md_file, + ], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + process.communicate(None, timeout=10) + if process.returncode != 0: + raise RuntimeError(f"po2md failed with code {process.returncode}") + logger.info("Po file converted to md") + return [] + + +def main(): + language_manager.initialize(REPOSITORY_DIR / "languages.json") + with (REPOSITORY_DIR / "projects.txt").open("r", encoding="utf-8") as f: + project_dirs = [i for i in f.read().split("\n") if i] + for project_dir in project_dirs: + process_project(PROJECTS_DIR / project_dir) + messages = message_manager.get_messages() + if not messages: + messages = [ + "Validation and conversion completed without errors, let's wait for the review team.\n" + "Thanks for your contribution!" + ] + with (REPOSITORY_DIR / "result.txt").open( + "w", encoding="utf-8", newline="" + ) as f: + f.write("\n".join(messages)) + + +if __name__ == "__main__": + main() diff --git a/scripts/utils.py b/scripts/utils.py new file mode 100644 index 0000000..60db18d --- /dev/null +++ b/scripts/utils.py @@ -0,0 +1,51 @@ +import os +import shlex +import subprocess +from pathlib import Path + +from constants import ( + BRACES_PLACEHOLDER_REGEX, + PERCENT_PLACEHOLDER_REGEX, + RESX_FILE_REGEX, +) + + +def _percent_to_braces(match): + n = int(match[1]) + return f"{{{n-1}}}" # {0} for example + + +def _braces_to_percent(match): + n = int(match[1]) + return f"%{n+1}" + + +def convert_braces_to_percents(string): + return BRACES_PLACEHOLDER_REGEX.sub(_braces_to_percent, string) + + +def convert_percents_to_braces(string): + return PERCENT_PLACEHOLDER_REGEX.sub(_percent_to_braces, string) + + +def file_updated(file_path): + file_path = Path(file_path).resolve() + quoted_file_path = shlex.quote(file_path.name) + previous_commit = os.environ.get("BEFORE_PUSH_COMMIT_SHA", "HEAD~1") + return_code = subprocess.call( + ["bash", "-c", f"[[ $(git diff {previous_commit} -- {quoted_file_path}) ]]"], + cwd=str(file_path.parent), + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + # 0 means we have some diff + return return_code == 0 + + +def parse_resx_filename(resx_filename): + match = RESX_FILE_REGEX.match(resx_filename) + return match[1], match[2] + + +def get_percent_placeholders(string): + return PERCENT_PLACEHOLDER_REGEX.findall(string)