Фиксы/Перевод (#296)

* upd

* upd

* upd

* add localization tools

* upd

* upd

* upd

* upd

* upd

* Update book-salvage.ftl

* Update book-authorbooks.ftl

* Update implant.ftl

* fix

* fix

* fix
This commit is contained in:
Rinary 2024-08-18 00:28:18 +03:00 committed by GitHub
parent 012be1afc2
commit 3fc5d92b2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
96 changed files with 1931 additions and 236 deletions

View file

@ -173,7 +173,8 @@ public sealed class SunriseCCVars
public static readonly CVarDef<float> LobbyOpacity =
CVarDef.Create("lobby.lobby_opacity", 0.90f, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<string> ServerName = CVarDef.Create("lobby.server_name", "SS14", CVar.REPLICATED);
public static readonly CVarDef<string> ServerName =
CVarDef.Create("lobby.server_name", "SS14", CVar.SERVER | CVar.REPLICATED);
/*
* Planet Prison

View file

@ -0,0 +1,32 @@
ent-ActionInfect = Заразить [10 ОЭ]
.desc = Попытка заразить существо [-10 очков эволюции]. Первые три попытки заражения не требуют очков эволюции. Выберите способность и нажмите ЛКМ по сущности, чтобы заразить.
ent-ActionDiseaseShop = Мутация
.desc = Открывает меню мутации
ent-ActionDiseaseBaseChance = Улучшение заражения. [20 ОЭ]
.desc = Действие 'заразить' имеет больший шанс на успех. [-20 очков эволюции]
ent-ActionDiseaseInfectChance = Повышение заразности. [15 ОЭ]
.desc = Увеличивает заразность вируса. Влияет на шанс заражения. [-15 очков эволюции]
ent-ActionDiseaseShield = Повышение устойчивости. [15 ОЭ]
.desc = Повышает устойчивость болезни к лекарствам. Влияет на скорость лечения при употреблении лекарства. [-15 очков эволюции]
ent-ActionDiseaseLethal = Повышение летальности. [15 ОЭ]
.desc = Увеличивает получаемый урон от симптомов. Влияет на получаемый при кашле урон. [-15 очков эволюции]
ent-ActionDiseaseCough = Кашель
.desc = Заражённые начинают кашлять.
ent-ActionDiseaseSneeze = Чих
.desc = Заражённые начинают чихать.
ent-ActionDiseaseVomit = Тошнота
.desc = Заражённых начинает тоншить, вызывая рвоту.
ent-ActionDiseaseNarcolepsy = Сонливость
.desc = У заражённых появляется постоянное желание спать, с которым они иногда не могут справиться.
ent-ActionDiseaseCrying = Непроизвольные слёзы
.desc = У заражённых активно слезятся глаза, из-за чего кажется, что они плачут.
ent-ActionDiseaseMuted = Немота
.desc = Мутация вызывает повреждение подъязычного нерва, приводя к параличу мышц языка, из-за чего больные теряют возможность нормально говорить.
ent-ActionDiseaseSlowness = Изнеможение
.desc = Вирус вызывает разрушение мышечных волокон, приводящее к атрофие и сопровождающееся слабостью. Снижает общую мобильность
ent-ActionDiseaseBleed = Кровопотеря
.desc = Вирус вызывает денатурацию гемоглобина крови, из-за чего у всех носителей появляется тяжелая степень анемии
ent-ActionDiseaseBlindness = Слепота
.desc = Длительная болезнь приводит к отмиранию зрительного нерва, что приводит к практически полной слепоте больного.
ent-ActionDiseaseInsult = Судороги
.desc = Длительная болезнь вызывает гиперстимуляцию двигательных нейронов, в результате чего больные могут испытывать перенапряжение мышц, приводящие к судорогам.

View file

@ -4,3 +4,5 @@ ent-ClothingHeadsetMagistrate = гарнитура Магистрата
.desc = Гарнитура магистрата, чтобы услышать последние слова зеков.
ent-ClothingHeadsetLawyer = гарнитура Юриста
.desc = Гарнитура юриста, что-бы защитить честь клоуна.
ent-ClothingHeadsetJEngineering = гарнитура тюремного инженерного отдела.
.desc = Гарнитура, которую используют тюремные инженеры.

View file

@ -2,3 +2,5 @@ ent-FoodSnackMaysonEnergy = энергетический батончик Мэй
.desc = Энергетический батончик состоящий из натуральных химикатов!
ent-FoodSnackMaysonEnergyBar = энергетический батончик Мэйсона
.desc = Энергетический батончик состоящий из натуральных химикатов!
ent-FoodPacketMaysonEnergyTrash = обёртка энергетического батончика
.desc = { ent-FoodPacketTrash.desc }

View file

@ -1,2 +1,4 @@
ent-Boris = роботический мозг
.desc = Искусственный мозг, способной к банальной интелектуальной активности.
ent-Sofia = роботический мозг синдиката
.desc = Искусственный мозг, способной к банальной интелектуальной активности.

View file

@ -1,2 +0,0 @@
ent-Boris = роботический мозг
.desc = Искусственный мозг, способной к банальной интелектуальной активности.

View file

@ -1,2 +0,0 @@
ent-EvilTwinSpawn = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }

View file

@ -1,2 +0,0 @@
ent-EvilTwin = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }

View file

@ -1,2 +0,0 @@
ent-FugitiveSpawn = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }

View file

@ -1,2 +0,0 @@
ent-Fugitive = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }

View file

@ -0,0 +1,6 @@
ent-DiseaseSpawn = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }
ent-FugitiveSpawn = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }
ent-EvilTwinSpawn = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }

View file

@ -0,0 +1,6 @@
ent-DiseaseRule = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }
ent-Fugitive = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }
ent-EvilTwin = { ent-BaseGameRule }
.desc = { ent-BaseGameRule.desc }

View file

@ -0,0 +1,2 @@
ent-MobDisease = разумная болезнь
.desc = plague inc 2.0

View file

@ -0,0 +1,10 @@
ent-StatueFurukai = статуя Фурукай
.desc = { ent-BaseStructure.desc }
ent-StatueDude = статуя Дюда
.desc = { ent-BaseStructure.desc }
ent-StatueFrank = статуя Фрэнка
.desc = { ent-BaseStructure.desc }
ent-StatueMooniverse = статуя Лунивёрса
.desc = { ent-BaseStructure.desc }
ent-StatueRib = статуя Риба
.desc = { ent-BaseStructure.desc }

View file

@ -2,10 +2,6 @@ mob-name-disease = Разумная болезнь
mob-description-disease = plague inc 2.0
ghostrole-disease-name = разумная болезнь
ghostrole-disease-description = Обычная болезнь, которая вдруг решила стать разумной. С кем не бывает?
action-disease-infect-name = Заразить [10 ОЭ]
action-disease-infect-description = Попытка заразить существо [-10 очков эволюции]. Первые три попытки заражения не требуют очков эволюции. Выберите способность и нажмите ЛКМ по сущности, чтобы заразить.
action-disease-shop-name = Мутация
action-disease-shop-description = Открывает меню мутации
disease-round-end-result = [color=green]На станции была разумная болезнь. Ей было:[/color]
disease-round-end-result-infected = Заражено: [color=gold]{ $count }[/color]
disease-round-end-result-infects = Заражённых в конце раунда: [color=gold]{ $count }[/color]

View file

@ -2,33 +2,5 @@ shop-disease-category-infect = Передача
shop-disease-category-symptoms = Симптомы
shop-disease-category-evolution = Улучшение
shop-disease-currency = Очки эволюции
listing-disease-cough-name = Кашель
listing-disease-cough-description = Заражённые начинают кашлять.
listing-disease-sneeze-name = Чих
listing-disease-sneeze-description = Заражённые начинают чихать.
listing-disease-vomit-name = Тошнота
listing-disease-vomit-description = Заражённых начинает тоншить, вызывая рвоту.
listing-disease-zombie-name = Зомбирование
listing-disease-zombie-description = 70% текущих заражённых становятся зомби. Они так-же перестают быть заражёнными.
listing-disease-cry-name = Непроизвольные слёзы
listing-disease-cry-description = У заражённых активно слезятся глаза, из-за чего кажется, что они плачут.
listing-disease-base-chance-name = Улучшение заражения. [20 ОЭ]
listing-disease-base-chance-description = Действие 'заразить' имеет больший шанс на успех. [-20 очков эволюции]
listing-disease-infect-chance-name = Повышение заразности. [15 ОЭ]
listing-disease-infect-chance-description = Увеличивает заразность вируса. Влияет на шанс заражения. [-15 очков эволюции]
listing-disease-shield-name = Повышение устойчивости. [15 ОЭ]
listing-disease-shield-description = Повышает устойчивость болезни к лекарствам. Влияет на скорость лечения при употреблении лекарства. [-15 очков эволюции]
listing-disease-lethal-name = Повышение летальности. [15 ОЭ]
listing-disease-lethal-description = Увеличивает получаемый урон от симптомов. Влияет на получаемый при кашле урон. [-15 очков эволюции]
listing-disease-narcolepsy-name = Сонливость
listing-disease-narcolepsy-description = У заражённых появляется постоянное желание спать, с которым они иногда не могут справиться.
listing-disease-muted-name = Немота
listing-disease-muted-description = Мутация вызывает повреждение подъязычного нерва, приводя к параличу мышц языка, из-за чего больные теряют возможность нормально говорить.
listing-disease-slowness-name = Изнеможение
listing-disease-slowness-description = Вирус вызывает разрушение мышечных волокон, приводящее к атрофие и сопровождающееся слабостью. Снижает общую мобильность
listing-disease-bleed-name = Кровопотеря
listing-disease-bleed-description = Вирус вызывает денатурацию гемоглобина крови, из-за чего у всех носителей появляется тяжелая степень анемии
listing-disease-blindness-name = Слепота
listing-disease-blindness-description = Длительная болезнь приводит к отмиранию зрительного нерва, что приводит к практически полной слепоте больного.
listing-disease-insult-name = Судороги
listing-disease-insult-description = Длительная болезнь вызывает гиперстимуляцию двигательных нейронов, в результате чего больные могут испытывать перенапряжение мышц, приводящие к судорогам.

View file

@ -1,2 +0,0 @@
ent-ClothingHeadsetJEngineering = гарнитура тюремного инженерного отдела.
.desc = Гарнитура, которую используют тюремные инженеры.

View file

@ -0,0 +1,134 @@
accent-german-words-1 = да
accent-german-words-replace-1 = ja
accent-german-words-2 = нет
accent-german-words-replace-2 = nein
accent-german-words-3 = тот
accent-german-words-replace-3 = Das
accent-german-words-4 = дерьмо
accent-german-words-replace-4 = scheisse
accent-german-words-5 = сосиска
accent-german-words-replace-5 = wurst
accent-german-words-6 = сосиски
accent-german-words-replace-6 = würste
accent-german-words-7 = мужик
accent-german-words-replace-7 = mann
accent-german-words-8 = мужики
accent-german-words-replace-8 = männer
accent-german-words-9 = женщина
accent-german-words-9-2 = леди
accent-german-words-replace-9 = frau
accent-german-words-10 = женщины
accent-german-words-10-2 = дамы
accent-german-words-replace-10 = frauen
accent-german-words-11 = джентельмен
accent-german-words-replace-11 = herr
accent-german-words-12 = джентельмены
accent-german-words-replace-12 = herren
accent-german-words-13 = мой бог
accent-german-words-replace-13 = mein gott
accent-german-words-14 = мой
accent-german-words-replace-14 = mein
accent-german-words-15 = здесь
accent-german-words-replace-15 = hier
accent-german-words-16 = идиот
accent-german-words-replace-16 = dummkopf
accent-german-words-17 = идиоты
accent-german-words-replace-17 = dummköpfe
accent-german-words-18 = бабочка
accent-german-words-replace-18 = schmetterling
accent-german-words-19 = машина
accent-german-words-replace-19 = maschine
accent-german-words-replace-19-2 = auto
accent-german-words-20 = машины
accent-german-words-replace-20 = maschinen
accent-german-words-21 = осторожно
accent-german-words-replace-21 = achtung
accent-german-words-22 = музыка
accent-german-words-replace-22 = musik
accent-german-words-23 = капитан
accent-german-words-replace-23 = kapitän
accent-german-words-24 = кебаб
accent-german-words-replace-24 = döner
accent-german-words-25 = мышь
accent-german-words-replace-25 = maus
accent-german-words-26 = что
accent-german-words-replace-26 = wat
accent-german-words-27 = спасибо вам
accent-german-words-replace-27 = dankeschön
accent-german-words-28 = спасибо
accent-german-words-replace-28 = danke
accent-german-words-29 = будьте здоровы
accent-german-words-replace-29 = gesundheit
accent-german-words-30 = огнемёт
accent-german-words-replace-30 = flammenwerfer
accent-german-words-31 = призрак
accent-german-words-replace-31 = poltergeist
accent-german-words-32 = сорняк
accent-german-words-32-2 = cabbage
accent-german-words-replace-32 = kraut
accent-german-words-33 = вотка
accent-german-words-replace-33 = wodka
accent-german-words-34 = рюкзак
accent-german-words-replace-34 = rucksack
accent-german-words-35 = медицина
accent-german-words-replace-35 = medizin
accent-german-words-36 = акцент
accent-german-words-replace-36 = akzent
accent-german-words-37 = аномалия
accent-german-words-replace-37 = anomalie
accent-german-words-38 = артефакт
accent-german-words-38-2 = artefact
accent-german-words-replace-38 = artefakt
accent-german-words-39 = тупой
accent-german-words-replace-39 = dumm
accent-german-words-40 = глупый
accent-german-words-replace-40 = doof
accent-german-words-41 = замечательный
accent-german-words-41-2 = прекрастный
accent-german-words-replace-41 = wunderbar
accent-german-words-42 = опастно
accent-german-words-replace-42 = warnung
accent-german-words-43 = опастности
accent-german-words-replace-43 = warnungen
accent-german-words-44 = и
accent-german-words-replace-44 = und
accent-german-words-45 = карп
accent-german-words-replace-45 = karpfen
accent-german-words-46 = командир
accent-german-words-replace-46 = kommandant
accent-german-words-47 = пиво
accent-german-words-replace-47 = bier
accent-german-words-48 = привет
accent-german-words-replace-48 = hallo
accent-german-words-49 = здравия
accent-german-words-replace-49 = guten Tag
accent-german-words-50 = помощь
accent-german-words-replace-50 = hilfe
accent-german-words-51 = до свидания
accent-german-words-replace-51 = auf Wiedersehen
accent-german-words-52 = пока
accent-german-words-replace-52 = tschüss
accent-german-words-53 = пока пока
accent-german-words-53-2 = пока-пока
accent-german-words-replace-53 = tschau
accent-german-words-54 = фантастически
accent-german-words-replace-54 = fantastisch
accent-german-words-55 = генокрад
accent-german-words-replace-55 = doppelgänger
accent-german-words-56 = запрещенный
accent-german-words-replace-56 = verboten
accent-german-words-57 = быстрый
accent-german-words-57-2 = быстро
accent-german-words-replace-57 = schnell
accent-german-words-58 = мед
accent-german-words-replace-58 = krankenhaus
accent-german-words-59 = катушка теслы
accent-german-words-replace-59 = tesla coil
accent-german-words-60 = катушки теслы
accent-german-words-replace-60 = tesla coils
accent-german-words-61 = teslaloose
accent-german-words-61-2 = tesloose
accent-german-words-61-3 = lightning ball
accent-german-words-61-4 = шаровая молния
accent-german-words-61-5 = тесла
accent-german-words-replace-61 = kugelblitz

View file

@ -5,7 +5,7 @@ player-panel-bans = Всего банов: { $totalBans }
player-panel-rolebans = Всего роль банов: { $totalRoleBans }
player-panel-notes = Всего заметок: { $totalNotes }
player-panel-playtime = Всего наиграно времени: { $days }д:{ $hours }ч:{ $minutes }м
player-panel-shared-connections = Shared Connections: { $sharedConnections }
player-panel-shared-connections = Общие подключения: { $sharedConnections }
player-panel-copy-username = Копировать
player-panel-show-notes = Заметки
player-panel-show-bans = Показать баны
@ -18,4 +18,5 @@ player-panel-ban = Бан
player-panel-logs = Логи
player-panel-delete = Удалить
player-panel-rejuvenate = Воскресить
player-panel-false = False
player-panel-false = Нет
player-panel-true = Да

View file

@ -1,5 +1,6 @@
## Entity
gas-analyzer-object-out-of-range = Объект вышел за пределы зоны обнаружения.
gas-analyzer-component-player-cannot-reach-message = Вы не можете туда достать.
gas-analyzer-shutoff = Газоанализатор выключается.

View file

@ -0,0 +1,2 @@
botany-plant-seedsrestored = Слышен едва уловимый звук шуршания листьев.
botany-plant-seedsdestroyed = Семена на растении начинают трещать и падать!

View file

@ -128,6 +128,7 @@ bounty-description-torch = Шеф-повар оживил всех обезья
bounty-description-medkit-box = Центком ставит спектакль в больнице, и нуждается в реквизите. Просто пришлите нам несколько пустых аптечек, и спектакль состоится!
bounty-description-cardobard-box = Завтра состоится премьера новой пьесы "Пришествие картонборга", а костюмеры совершенно не готовы. Пришлите нам несколько коробок для оформления.
bounty-description-wine = Новая библиотекарша и квартирмейстер влюбились друг в друга по уши после того, как она застала его за разборкой книжных полок на дрова. Пришлите пару бутылок вина (или банок, если хотите), чтобы свидание прошло удачно.
bounty-description-cardboard-box = "The Cardborgs Cometh" is a new play premiering tomorrow, and the costuming team is woefully unprepared. Send us some boxes to work with.
bounty-description-cotton-boll = Огромный рой таракамолей съел всю бумагу и ткань на станции. Пришлите нам немного хлопка, чтобы наши крылатые члены экипажа остались сыты.
bounty-description-microwave-machine-board = Мистер Хихстер решил, что будет забавно позасовывать металлические вилки во все кухонные микроволновки. Помогите нам отремонтировать их, пока повара не принялись готовить клоунбургеры.
bounty-description-lasergun = Караван утилизаторов запрашивает большую партию лазерного оружия для уничтожения улья ксеноморфов.

View file

@ -0,0 +1 @@
comp-self-unremovable-clothing = Это нельзя снять без посторонней помощи.

View file

@ -25,12 +25,18 @@ figurines-musician-2 = Never gonna let you down!
figurines-captain-1 = Экипаж, диск от ядерной бомбы надежно спрятан у меня в заднице.
figurines-captain-2 = Слава НТ!
figurines-captain-3 = Ядерный диск в безопасности. Где? Где-то.
figurines-hos-1 = Космический закон? Что?
figurines-hos-2 = Пристрелил клоуна.
figurines-hos-3 = Да, я выстрелил в клоуна. Нет, я не жалею об этом.
figurines-warden-1 = Казните его за взлом!
figurines-warden-2 = Навсегда накажем этого урода за оскорбление!
figurines-warden-3 = Мы абсолютно честно обращаемся со всеми и НЕ злоупотребляем нашими заключенными.
figurines-detective-1 = Это сделал дворецкий.
figurines-detective-2 = Мне нужно немного виски после этого.
figurines-security-1 = Я и есть закон!
figurines-security-2 = Вы нарушили статью 1984.
figurines-security-3 = Когда мне становится скучно, я использую клоуна для стрельбы.
figurines-lawyer-1 = Лучше позвоните Солу!
figurines-lawyer-2 = Протестую!
figurines-cargotech-1 = ДРАКОН НА АТЗ!

View file

@ -74,6 +74,8 @@ ui-options-scale-175 = 175%
ui-options-scale-200 = 200%
ui-options-hud-theme = Тема HUD:
ui-options-hud-theme-default = По умолчанию
ui-options-hud-layout-default = Обычный
ui-options-hud-layout-separated = Раздельный
ui-options-hud-theme-plasmafire = Плазма
ui-options-hud-theme-slimecore = Слаймкор
ui-options-hud-theme-clockwork = Механизм
@ -149,6 +151,9 @@ ui-options-static-storage-ui = Закрепить интерфейс храни
ui-options-function-smart-equip-backpack = Умная экипировка в рюкзак
ui-options-function-smart-equip-belt = Умная экипировка на пояс
ui-options-function-open-backpack = Открыть рюкзак
ui-options-function-rotate-object-clockwise = Вращать по часовой стрелке
ui-options-function-rotate-object-counterclockwise = Вращать против часовой стрелки
ui-options-function-flip-object = Повернуть
ui-options-function-open-belt = Открыть пояс
ui-options-function-throw-item-in-hand = Бросить предмет
ui-options-function-try-pull-object = Тянуть объект

View file

@ -1,2 +1,4 @@
all-at-once-title = Всё и сразу
all-at-once-description = Это просто не ваш день...
aller-at-once-title = Всё и вся сразу
aller-at-once-description = Вы теперь серьёзно накосячили. Теперь вы *действительно* накосячили.

View file

@ -1,2 +1,4 @@
survival-title = Выживание
survival-description = Внутренние угрозы отсутствуют, но как долго станция сможет продержаться в обстановке всё более разрушительных и частых событий?
kessler-syndrome-title = Синдром Кесслера
kessler-syndrome-description = Нет внутренних угроз, но станция быстро попадает в пояс метеоров!

View file

@ -1,5 +1,7 @@
zombie-title = Зомби
zombie-description = На станции появились ожившие мертвецы! Работайте сообща с другими членами экипажа, чтобы пережить эпидемию и защитить станцию.
zombieteors-title = Зомбитеоры
zombieteors-description = Нежить была выпущена на станцию на фоне катастрофического метеоритного дождя! Работайте вместе с вашим экипажем и делайте все возможное, чтобы выжить!
zombie-not-enough-ready-players = Недостаточно игроков готовы к игре! { $readyPlayersCount } игроков из необходимых { $minimumPlayers } готовы. Нельзя запустить пресет Зомби.
zombie-no-one-ready = Нет готовых игроков! Нельзя запустить пресет Зомби.
zombie-patientzero-role-greeting = Вы — нулевой пациент. Снарядитесь и подготовьтесь к своему превращению. Ваша цель - захватить станцию, заразив при этом как можно больше членов экипажа.

View file

@ -351,3 +351,13 @@ reagent-effect-guidebook-plant-robust-harvest =
[1] Повышает
*[other] повышают
} потенцию растения путём { $increase } до максимума в { $limit }. Приводит к тому, что растение теряет свои семена, когда потенция достигает { $seedlesstreshold }. Попытка повысить потенцию свыше { $limit } может вызвать снижение урожайности с вероятностью 10%.
reagent-effect-guidebook-plant-seeds-add =
{ $chance ->
[1] Restores the
*[other] restore the
} seeds of the plant
reagent-effect-guidebook-plant-seeds-remove =
{ $chance ->
[1] Removes the
*[other] remove the
} seeds of the plant

View file

@ -1,5 +1,6 @@
plant-attribute-growth = возраст
plant-attribute-water = уровень воды
plant-attribute-potency = потенция
plant-attribute-weeds = уровень сорняков
plant-attribute-toxins = уровень токсинов
plant-attribute-nutrition = уровень питательных веществ

View file

@ -1,7 +1,79 @@
food-sequence-no-space = Вы не можете положить больше!
food-sequence-standart-gen = { $prefix }{ $content }{ $suffix }
food-sequence-content-chicken = курица
food-sequence-content-duck = утка
food-sequence-content-crab = крабы
food-sequence-content-dragon = дракон
food-sequence-content-snake = змея
food-sequence-content-xeno = ксеноморф
food-sequence-content-rouny = роуни
food-sequence-content-tomato = помидор
food-sequence-content-salami = салями
food-sequence-content-slime = слизь
food-sequence-content-clown = клоун
food-sequence-content-pea = горох
food-sequence-content-bungo = бунго
food-sequence-content-banana = банан
food-sequence-content-mimana = мимана
food-sequence-content-lemon = лимон
food-sequence-content-lemoon = лему́н
food-sequence-content-lime = лайм
food-sequence-content-orange = апельсин
food-sequence-content-potato = картофель
food-sequence-content-apple = яблоко
food-sequence-content-cocoa = какао
food-sequence-content-corn = кукуруза
food-sequence-content-chili = чили
food-sequence-content-chilly = охлаждённый
food-sequence-content-mushroom = грибы
food-sequence-content-aloe = алоэ
food-sequence-content-poppy = мак
food-sequence-content-lily = лилия
food-sequence-content-soy = соя
food-sequence-content-cheese = сыр
food-sequence-content-chevre = chèvre
food-sequence-content-tofu = тофу
food-sequence-content-brain = мозг
food-sequence-content-tongue = язык
food-sequence-content-ears = ухо
food-sequence-content-stomach = желудок
food-sequence-content-liver = печень
food-sequence-content-clump = ком
food-sequence-content-raw-meat = сырое мясо
food-sequence-content-meat = мясо
food-sequence-content-carp = карп
food-sequence-content-bear = медвежатина
food-sequence-content-penguin = мясо пингвина
food-sequence-content-corgi = мясо корги
food-sequence-content-goliath = мясо голиафа
food-sequence-content-rat = крыса
food-sequence-content-lizard = мясо ящерицы
food-sequence-content-plant = растительное мясо
food-sequence-content-rotten = гнилое мясо
food-sequence-content-spider = паучьи лапки
food-sequence-content-carrot = морковь
food-sequence-content-cabbage = капуста
food-sequence-content-garlic = чеснок
food-sequence-content-pineapple = ананас
food-sequence-content-onion = лук
food-sequence-content-ambrosia = амброзия
food-sequence-content-galaxy = галактический чертополох
food-sequence-content-glasstle = гластл
food-sequence-content-gatfruit = гатфрут
food-sequence-content-koibean = койбина
food-sequence-content-watermelon = арбуз
food-sequence-content-cannabis = каннабис
food-sequence-content-rainbow-cannabis = радужный каннабис
food-sequence-content-tobacco = табак
food-sequence-content-hamster = хомяк
food-sequence-content-suppermatter = супермасса
food-sequence-content-capfruit = капфрут
food-sequence-content-berries = ягоды
food-sequence-content-spacemans-trumpet = космический духовой инструмент
#Burger
food-sequence-burger-gen = { $content }бургер
food-sequence-burger-suffix = бургер
food-sequence-burger-content-meat = мясно
food-sequence-burger-content-raw-meat = сырой
food-sequence-burger-content-carp = карпо
food-sequence-burger-content-bear = медведь
@ -21,6 +93,7 @@ food-sequence-burger-content-clump = комок
food-sequence-burger-content-snake = змея
food-sequence-burger-content-xeno = ксено
food-sequence-burger-content-tomato = томат
food-sequence-burger-content-capfruit = кап
food-sequence-burger-content-salami = салями
food-sequence-burger-content-clown = клоун
food-sequence-burger-content-slime = слайм
@ -28,8 +101,14 @@ food-sequence-burger-content-pea = пеа
food-sequence-burger-content-banana = банан
food-sequence-burger-content-mimana = мимана
food-sequence-burger-content-carrot = морковно
food-sequence-burger-content-suppermatter = суппер
food-sequence-burger-content-hamster = хомя
food-sequence-burger-content-berries = ягодно
food-sequence-burger-content-spacemans-trumpet = космострубко
food-sequence-burger-content-cabbage = капуста
food-sequence-taco-gen = тако с { $content }
food-sequence-burger-content-garlic = чесночно
food-sequence-skewer-gen = { $content } кебаб
food-sequence-burger-content-lemon = лемон
food-sequence-burger-content-orange = апельсино
food-sequence-burger-content-pineapple = ананасо

View file

@ -1,11 +1,13 @@
power-radiation-collector-gas-tank-missing = Выемка для баллона с плазмой [color=darkred]пустует[/color].
power-radiation-collector-gas-tank-present = Выемка для баллона с плазмой [color=darkgreen]заполнена[/color] и индикатор баллона находится на отметке [color={$fullness ->
*[0]red]пусто
[1]red]мало
[2]yellow]заполнено наполовину
[3]lime]заполнено
}[/color].
power-radiation-collector-enabled = Находится в режиме [color={$state ->
power-radiation-collector-gas-tank-present =
Выемка для баллона с плазмой [color=darkgreen]заполнена[/color] и индикатор баллона находится на отметке [color={ $fullness ->
*[0] red]пусто
[1] red]мало
[2] yellow]заполнено наполовину
[3] lime]заполнено
}[/color].
power-radiation-collector-enabled =
Находится в режиме [color={ $state ->
[true] darkgreen]вкл
*[false] darkred]выкл
}[/color].
}[/color].

View file

@ -31,6 +31,7 @@ loadout-group-passenger-mask = Пассажир, маска
loadout-group-passenger-gloves = Пассажир, перчатки
loadout-group-passenger-outerclothing = Пассажир, верхняя одежда
loadout-group-passenger-shoes = Пассажир, обувь
loadout-group-passenger-neck = Пассажир, шея
loadout-group-bartender-head = Бармен, голова
loadout-group-bartender-jumpsuit = Бармен, комбинезон
loadout-group-bartender-outerclothing = Бармен, верхняя одежда
@ -133,6 +134,7 @@ loadout-group-detective-neck = Детектив, шея
loadout-group-detective-jumpsuit = Детектив, комбинезон
loadout-group-detective-outerclothing = Детектив, верхняя одежда
loadout-group-security-cadet-jumpsuit = Кадет СБ, комбинезон
loadout-group-security-star = Звезда СБ
# Medical
loadout-group-medical-gloves = Медицинский отдел, перчатки
loadout-group-medical-mask = Медицинский отдел, маска

View file

@ -10,6 +10,7 @@ marking-used = { $marking-name }
marking-used-forced = { $marking-name } (Принудительно)
marking-slot-add = Добавить
marking-slot-remove = Удалить
marking-slot = Слот { $number }
# Categories

View file

@ -14,3 +14,5 @@ reagent-name-ammonia = аммиак
reagent-desc-ammonia = Эффективное удобрение, дающее растениям питательные вещества.
reagent-name-diethylamine = диэтиламин
reagent-desc-diethylamine = Очень мощное удобрение, насыщает растения питательными веществами, уничтожает вредителей, а иногда даже ускоряет рост.
reagent-name-sedin = седин
reagent-desc-sedin = Модифицированная версия диэтилового аммиака, которая может восстанавливать семена на растениях за счет силы действия.

View file

@ -24,6 +24,7 @@ research-technology-biochemical-stasis = Биохимический стазис
research-technology-mechanized-treatment = Механизированная подготовка
research-technology-bluespace-chemistry = Блюспейс-химия
research-technology-salvage-weapons = Утилизаторское оружие
research-technology-special-means = Специальные средства
research-technology-draconic-munitions = Драконьи боеприпасы
research-technology-nonlethal-ammunition = Не летальная аммуниция
research-technology-uranium-munitions = Урановые боеприпасы

View file

@ -32,6 +32,12 @@ dungeon-config-proto-ClusterAsteroid = Астероидный кластер
dungeon-config-proto-SpindlyAsteroid = Астероидная спираль
dungeon-config-proto-SwissCheeseAsteroid = Фрагменты астероидов
# Wrecks
salvage-map-wreck = Спасательный обломок
salvage-map-wreck-desc-size = Размер:
salvage-map-wreck-size-small = [color=lime]Маленький[/color]
salvage-map-wreck-size-medium = [color=cornflowerblue]Средний[/color]
salvage-map-wreck-size-large = [color=orchid]Большой[/color]
# Wrecks
salvage-map-proto-Small1 = Инженерное хранилище
salvage-map-proto-Small2 = Игровой уголок
salvage-map-proto-Small3 = Прачечная

View file

@ -122,3 +122,5 @@ seeds-cotton-name = хлопок
seeds-cotton-display-name = куст хлопка
seeds-pyrotton-name = пирохлопок
seeds-pyrotton-display-name = куст пирохлопка
seeds-capfruit-name = капфрут
seeds-capfruit-display-name = дерево капфрута

View file

@ -0,0 +1,3 @@
ent-RandomIngredient = спавнер случайных ингредиентов
.suffix = Не растительные
.desc = { ent-MarkerBase.desc }

View file

@ -0,0 +1,2 @@
ent-RandomMeat = спавнер случайного мяса
.desc = { ent-MarkerBase.desc }

View file

@ -1,7 +1,5 @@
ent-SalvageMaterialCrateSpawner = спавнер обломок ящик материалов
.desc = { ent-MarkerBase.desc }
ent-SalvageCanisterSpawner = спавнер обломок канистра
.desc = { ent-MarkerBase.desc }
ent-SalvageLootSpawner = спавнер обломок добыча
.desc = { ent-MarkerBase.desc }
ent-SalvageMobSpawner = спавнер обломок моб

View file

@ -0,0 +1,37 @@
ent-SalvageSpawnerScrapCommon = Спавнер металлолома
.suffix = Обычный, 50%
.desc = { ent-MarkerBase.desc }
ent-SalvageSpawnerScrapCommon75 = { ent-SalvageSpawnerScrapCommon }
.suffix = Обычный, 75%
.desc = { ent-SalvageSpawnerScrapCommon.desc }
ent-SalvageSpawnerScrapValuable = Спавнер металлолома
.suffix = Ценный, 50%
.desc = { ent-MarkerBase.desc }
ent-SalvageSpawnerScrapValuable75 = { ent-SalvageSpawnerScrapValuable }
.suffix = Ценный, 75%
.desc = { ent-SalvageSpawnerScrapValuable.desc }
ent-SalvageSpawnerTreasure = Спавнер сокровищ
.suffix = Обычный, 75%
.desc = { ent-MarkerBase.desc }
ent-SalvageSpawnerTreasureValuable = Спавнер сокровищ
.suffix = Ценный, 75%
.desc = { ent-MarkerBase.desc }
ent-SalvageSpawnerEquipment = Спавнер снаряжения
.suffix = Обычный, 75%
.desc = { ent-MarkerBase.desc }
ent-SalvageSpawnerEquipmentValuable = Спавнер снаряжения
.suffix = Ценный, 75%
.desc = { ent-MarkerBase.desc }
ent-SalvageCanisterSpawner = спавнер обломок канистра
.desc = { ent-MarkerBase.desc }
ent-SalvageSpawnerMobMagnet = Спавнер мобов-магнитов
.suffix = 50%
.desc = { ent-MarkerBase.desc }
ent-SalvageSpawnerMobMagnet75 = { ent-SalvageSpawnerMobMagnet }
.suffix = 75%
.desc = { ent-SalvageSpawnerMobMagnet.desc }
ent-SalvageSpawnerMobMagnet100 = { ent-SalvageSpawnerMobMagnet }
.suffix = 100%
.desc = { ent-SalvageSpawnerMobMagnet.desc }
ent-SalvageSpawnerMobShark = Спавнер акуломиннов
.desc = { ent-MarkerBase.desc }

View file

@ -0,0 +1,22 @@
ent-TreasureDatadiskEncrypted = зашифрованный дата-диск
.desc = Старый дата-диск. Кто-то более сведущий, чем вы, мог бы его расшифровать и узнать что-то интересное. Всё, что вы можете сделать — это продать его.
ent-TreasureHardDiskDrive = жёсткий диск
.desc = Дисковый накопитель, предназначенный для старых земных технологий, примерно 1980 года. Возможно, найдётся какой-то коллекционер, который захочет заполучить это.
ent-TreasureFlopDiskDrive = дисковод для гибких дисков
.desc = Дисковод для чтения информации с гибких дисков. Жаль, что в наши дни на них хранятся только операции ядерных бомб.
ent-TreasureCDDrive = CD-дисковод
.desc = Устройство для чтения данных с компакт-дисков. В наше время это не самое полезное устройство, если вы, конечно, не собираетесь куда-то лететь.
ent-TreasureCPUSupercharged = сверхзаряженный процессор
.desc = Какая-то супер-инопланетная космотехника. Жаль, что все компьютеры уже оснащены процессорами.
ent-TreasureSampleTube = пробирка с образцом
.desc = Стеклянная пробирка с какой-то субстанцией. Выглядит хрупкой.
ent-TreasureCoinIron = монета
.desc = Плоский кусок металла. Если бы она всё ещё была в обращении, инфляция сделала бы её бесполезной. Но так как это не так, она стоит достаточно, чтобы хранить её в книге.
ent-TreasureCoinSilver = { ent-TreasureCoinIron }
.desc = { ent-TreasureCoinIron.desc }
ent-TreasureCoinGold = { ent-TreasureCoinIron }
.desc = { ent-TreasureCoinIron.desc }
ent-TreasureCoinAdamantine = { ent-TreasureCoinIron }
.desc = { ent-TreasureCoinIron.desc }
ent-TreasureCoinDiamond = { ent-TreasureCoinIron }
.desc = { ent-TreasureCoinIron.desc }

View file

@ -31,5 +31,7 @@ trait-liar-name = Патологический лжец
trait-liar-desc = Вы с трудом заставляете себя говорить правду. Иногда вы всё равно лжёте.
trait-cowboy-name = Ковбойский акцент
trait-cowboy-desc = Вы говорите с заметным ковбойским акцентом!
trait-german-name = Немецкий акцент
trait-german-desc = Похоже, вы родом из космической Германии.
trait-italian-name = Итальянский акцент
trait-italian-desc = Мамма миа! Похоже, вы жили в Космической Италии!

View file

@ -1,3 +1,88 @@
- type: entity
id: ActionInfect
name: action-disease-infect-name
description: action-disease-infect-description
noSpawn: true
components:
- type: EntityTargetAction
useDelay: 1
itemIconStyle: BigAction
whitelist:
components:
- HumanoidAppearance
canTargetSelf: false
interactOnMiss: false
icon:
sprite: _Sunrise/Actions/disease.rsi
state: action
event: !type:InfectEvent
- type: entity
id: ActionDiseaseShop
name: action-disease-shop-name
description: action-disease-shop-description
noSpawn: true
components:
- type: InstantAction
icon:
sprite: _Sunrise/Actions/disease.rsi
state: shop
event: !type:DiseaseShopActionEvent
- type: entity
id: ActionDiseaseBaseChance
name: listing-disease-base-chance-name
description: listing-disease-base-chance-description
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon:
sprite: _Sunrise/Actions/disease.rsi
state: baseChance
event: !type:DiseaseAddBaseChanceEvent
- type: entity
id: ActionDiseaseInfectChance
name: listing-disease-infect-chance-name
description: listing-disease-infect-chance-description
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon:
sprite: _Sunrise/Actions/disease.rsi
state: coughChance
event: !type:DiseaseAddCoughChanceEvent
- type: entity
id: ActionDiseaseShield
name: listing-disease-shield-name
description: listing-disease-shield-description
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon:
sprite: _Sunrise/Actions/disease.rsi
state: shield
event: !type:DiseaseAddShieldEvent
- type: entity
id: ActionDiseaseLethal
name: listing-disease-lethal-name
description: listing-disease-lethal-description
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon:
sprite: _Sunrise/Actions/disease.rsi
state: lethal
event: !type:DiseaseAddLethalEvent
#Store
- type: entity
id: ActionDiseaseCough
name: listing-disease-cough-name
@ -148,3 +233,4 @@
symptom: "Insult"
minLevel: 2

View file

@ -1,82 +0,0 @@
- type: entity
id: ActionInfect
name: action-disease-infect-name
description: action-disease-infect-description
noSpawn: true
components:
- type: EntityTargetAction
useDelay: 1
itemIconStyle: BigAction
whitelist:
components:
- HumanoidAppearance
canTargetSelf: false
interactOnMiss: false
icon:
sprite: _Sunrise/Actions/disease.rsi
state: action
event: !type:InfectEvent
- type: entity
id: ActionDiseaseShop
name: action-disease-shop-name
description: action-disease-shop-description
noSpawn: true
components:
- type: InstantAction
icon:
sprite: _Sunrise/Actions/disease.rsi
state: shop
event: !type:DiseaseShopActionEvent
- type: entity
id: ActionDiseaseBaseChance
name: listing-disease-base-chance-name
description: listing-disease-base-chance-description
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon:
sprite: _Sunrise/Actions/disease.rsi
state: baseChance
event: !type:DiseaseAddBaseChanceEvent
- type: entity
id: ActionDiseaseInfectChance
name: listing-disease-infect-chance-name
description: listing-disease-infect-chance-description
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon:
sprite: _Sunrise/Actions/disease.rsi
state: coughChance
event: !type:DiseaseAddCoughChanceEvent
- type: entity
id: ActionDiseaseShield
name: listing-disease-shield-name
description: listing-disease-shield-description
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon:
sprite: _Sunrise/Actions/disease.rsi
state: shield
event: !type:DiseaseAddShieldEvent
- type: entity
id: ActionDiseaseLethal
name: listing-disease-lethal-name
description: listing-disease-lethal-description
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon:
sprite: _Sunrise/Actions/disease.rsi
state: lethal
event: !type:DiseaseAddLethalEvent

View file

@ -1,17 +0,0 @@
- type: emote
id: Vomit
name: Тошнота
category: Vocal
chatMessages: [тошнитыы]
- type: emote
id: Headache
name: Головная боль
category: Vocal
chatMessages: [голова болитыы]
- type: emote
id: Insult
name: Инсульт
category: Vocal
chatMessages: [инсультыы]

View file

@ -1,17 +0,0 @@
- type: storeCategory
id: DiseaseInfectCategory
name: shop-disease-category-infect
- type: storeCategory
id: DiseaseSymptomsCategory
name: shop-disease-category-symptoms
- type: storeCategory
id: DiseaseEvolutionCategory
name: shop-disease-category-evolution
- type: currency
id: DiseasePoints
displayName: shop-disease-currency
canWithdraw: false

View file

@ -1,15 +0,0 @@
- type: entity
parent: BaseGameRule
id: DiseaseSpawn
categories: [ HideSpawnMenu ]
components:
- type: StationEvent
minimumPlayers: 40
weight: 10
maxOccurrences: 1
duration: 1
earliestStart: 30
reoccurrenceDelay: 20
- type: LateJoinSpawnRule
prototype: MobDisease

View file

@ -1,6 +0,0 @@
- type: entity
id: DiseaseRule
parent: BaseGameRule
categories: [ HideSpawnMenu ]
components:
- type: DiseaseRule

View file

@ -1,11 +0,0 @@
- type: entity
parent: BaseGameRule
id: EvilTwinSpawn
categories: [ HideSpawnMenu ]
components:
- type: StationEvent
minimumPlayers: 2
weight: 10
duration: 1
- type: LateJoinSpawnRule
prototype: SpawnPointEvilTwin

View file

@ -1,6 +0,0 @@
- type: entity
id: EvilTwin
parent: BaseGameRule
categories: [ HideSpawnMenu ]
components:
- type: EvilTwinRule

View file

@ -1,12 +0,0 @@
- type: entity
parent: BaseGameRule
id: FugitiveSpawn
categories: [ HideSpawnMenu ]
components:
- type: StationEvent
weight: 10
duration: 1
earliestStart: 30
minimumPlayers: 10
- type: LateJoinSpawnRule
prototype: SpawnPointGhostFugitive

View file

@ -1,6 +0,0 @@
- type: entity
id: Fugitive
parent: BaseGameRule
categories: [ HideSpawnMenu ]
components:
- type: FugitiveRule

View file

@ -0,0 +1,39 @@
- type: entity
parent: BaseGameRule
id: DiseaseSpawn
categories: [ HideSpawnMenu ]
components:
- type: StationEvent
minimumPlayers: 40
weight: 10
maxOccurrences: 1
duration: 1
earliestStart: 30
reoccurrenceDelay: 20
- type: LateJoinSpawnRule
prototype: MobDisease
- type: entity
parent: BaseGameRule
id: FugitiveSpawn
categories: [ HideSpawnMenu ]
components:
- type: StationEvent
weight: 10
duration: 1
earliestStart: 30
minimumPlayers: 10
- type: LateJoinSpawnRule
prototype: SpawnPointGhostFugitive
- type: entity
parent: BaseGameRule
id: EvilTwinSpawn
categories: [ HideSpawnMenu ]
components:
- type: StationEvent
minimumPlayers: 2
weight: 10
duration: 1
- type: LateJoinSpawnRule
prototype: SpawnPointEvilTwin

View file

@ -0,0 +1,20 @@
- type: entity
id: DiseaseRule
parent: BaseGameRule
categories: [ HideSpawnMenu ]
components:
- type: DiseaseRule
- type: entity
id: Fugitive
parent: BaseGameRule
categories: [ HideSpawnMenu ]
components:
- type: FugitiveRule
- type: entity
id: EvilTwin
parent: BaseGameRule
categories: [ HideSpawnMenu ]
components:
- type: EvilTwinRule

View file

@ -11,7 +11,7 @@
- type: loadout
id: BlueShieldDuffel
equipment:
jumpsuit: ClothingBackpackDuffelBlueShield
back: ClothingBackpackDuffelBlueShield
- type: loadout
id: BlueShieldJumpskirt

View file

@ -92,3 +92,21 @@
id: SecurityThings
name: Служба Безопасности
priority: 5
- type: storeCategory
id: DiseaseInfectCategory
name: shop-disease-category-infect
- type: storeCategory
id: DiseaseSymptomsCategory
name: shop-disease-category-symptoms
- type: storeCategory
id: DiseaseEvolutionCategory
name: shop-disease-category-evolution
- type: currency
id: DiseasePoints
displayName: shop-disease-currency
canWithdraw: false

View file

@ -189,3 +189,21 @@
- хряк.
- хряк
- хряк!
- type: emote
id: Vomit
name: Тошнота
category: Vocal
chatMessages: [тошнитыы]
- type: emote
id: Headache
name: Головная боль
category: Vocal
chatMessages: [голова болитыы]
- type: emote
id: Insult
name: Инсульт
category: Vocal
chatMessages: [инсультыы]

71
Tools/_sunrise/Report.txt Normal file
View file

@ -0,0 +1,71 @@
Проверка локализации:
Не переведённая строка "accent-german-words-replace-1" в "accent\german.ftl", строка 2: accent-german-words-replace-1 = ja
Не переведённая строка "accent-german-words-replace-2" в "accent\german.ftl", строка 4: accent-german-words-replace-2 = nein
Не переведённая строка "accent-german-words-replace-3" в "accent\german.ftl", строка 6: accent-german-words-replace-3 = Das
Не переведённая строка "accent-german-words-replace-4" в "accent\german.ftl", строка 8: accent-german-words-replace-4 = scheisse
Не переведённая строка "accent-german-words-replace-5" в "accent\german.ftl", строка 10: accent-german-words-replace-5 = wurst
Не переведённая строка "accent-german-words-replace-6" в "accent\german.ftl", строка 12: accent-german-words-replace-6 = würste
Не переведённая строка "accent-german-words-replace-7" в "accent\german.ftl", строка 14: accent-german-words-replace-7 = mann
Не переведённая строка "accent-german-words-replace-8" в "accent\german.ftl", строка 16: accent-german-words-replace-8 = männer
Не переведённая строка "accent-german-words-9-2" в "accent\german.ftl", строка 18: accent-german-words-9-2 = lady
Не переведённая строка "accent-german-words-replace-9" в "accent\german.ftl", строка 19: accent-german-words-replace-9 = frau
Не переведённая строка "accent-german-words-10-2" в "accent\german.ftl", строка 21: accent-german-words-10-2 = ladies
Не переведённая строка "accent-german-words-replace-10" в "accent\german.ftl", строка 22: accent-german-words-replace-10 = frauen
Не переведённая строка "accent-german-words-replace-11" в "accent\german.ftl", строка 24: accent-german-words-replace-11 = herr
Не переведённая строка "accent-german-words-replace-12" в "accent\german.ftl", строка 26: accent-german-words-replace-12 = herren
Не переведённая строка "accent-german-words-replace-13" в "accent\german.ftl", строка 28: accent-german-words-replace-13 = mein gott
Не переведённая строка "accent-german-words-replace-14" в "accent\german.ftl", строка 30: accent-german-words-replace-14 = mein
Не переведённая строка "accent-german-words-replace-15" в "accent\german.ftl", строка 32: accent-german-words-replace-15 = hier
Не переведённая строка "accent-german-words-replace-16" в "accent\german.ftl", строка 34: accent-german-words-replace-16 = dummkopf
Не переведённая строка "accent-german-words-replace-17" в "accent\german.ftl", строка 36: accent-german-words-replace-17 = dummköpfe
Не переведённая строка "accent-german-words-replace-18" в "accent\german.ftl", строка 38: accent-german-words-replace-18 = schmetterling
Не переведённая строка "accent-german-words-replace-19" в "accent\german.ftl", строка 40: accent-german-words-replace-19 = maschine
Не переведённая строка "accent-german-words-replace-19-2" в "accent\german.ftl", строка 41: accent-german-words-replace-19-2 = auto
Не переведённая строка "accent-german-words-replace-20" в "accent\german.ftl", строка 43: accent-german-words-replace-20 = maschinen
Не переведённая строка "accent-german-words-replace-21" в "accent\german.ftl", строка 45: accent-german-words-replace-21 = achtung
Не переведённая строка "accent-german-words-replace-22" в "accent\german.ftl", строка 47: accent-german-words-replace-22 = musik
Не переведённая строка "accent-german-words-replace-23" в "accent\german.ftl", строка 49: accent-german-words-replace-23 = kapitän
Не переведённая строка "accent-german-words-replace-24" в "accent\german.ftl", строка 51: accent-german-words-replace-24 = döner
Не переведённая строка "accent-german-words-replace-25" в "accent\german.ftl", строка 53: accent-german-words-replace-25 = maus
Не переведённая строка "accent-german-words-replace-26" в "accent\german.ftl", строка 55: accent-german-words-replace-26 = wat
Не переведённая строка "accent-german-words-replace-27" в "accent\german.ftl", строка 57: accent-german-words-replace-27 = dankeschön
Не переведённая строка "accent-german-words-replace-28" в "accent\german.ftl", строка 59: accent-german-words-replace-28 = danke
Не переведённая строка "accent-german-words-replace-29" в "accent\german.ftl", строка 61: accent-german-words-replace-29 = gesundheit
Не переведённая строка "accent-german-words-replace-30" в "accent\german.ftl", строка 63: accent-german-words-replace-30 = flammenwerfer
Не переведённая строка "accent-german-words-replace-31" в "accent\german.ftl", строка 65: accent-german-words-replace-31 = poltergeist
Не переведённая строка "accent-german-words-32-2" в "accent\german.ftl", строка 67: accent-german-words-32-2 = cabbage
Не переведённая строка "accent-german-words-replace-32" в "accent\german.ftl", строка 68: accent-german-words-replace-32 = kraut
Не переведённая строка "accent-german-words-replace-33" в "accent\german.ftl", строка 70: accent-german-words-replace-33 = wodka
Не переведённая строка "accent-german-words-replace-34" в "accent\german.ftl", строка 72: accent-german-words-replace-34 = rucksack
Не переведённая строка "accent-german-words-replace-35" в "accent\german.ftl", строка 74: accent-german-words-replace-35 = medizin
Не переведённая строка "accent-german-words-replace-36" в "accent\german.ftl", строка 76: accent-german-words-replace-36 = akzent
Не переведённая строка "accent-german-words-replace-37" в "accent\german.ftl", строка 78: accent-german-words-replace-37 = anomalie
Не переведённая строка "accent-german-words-38-2" в "accent\german.ftl", строка 80: accent-german-words-38-2 = artefact
Не переведённая строка "accent-german-words-replace-38" в "accent\german.ftl", строка 81: accent-german-words-replace-38 = artefakt
Не переведённая строка "accent-german-words-replace-39" в "accent\german.ftl", строка 83: accent-german-words-replace-39 = dumm
Не переведённая строка "accent-german-words-replace-40" в "accent\german.ftl", строка 85: accent-german-words-replace-40 = doof
Не переведённая строка "accent-german-words-replace-41" в "accent\german.ftl", строка 88: accent-german-words-replace-41 = wunderbar
Не переведённая строка "accent-german-words-replace-42" в "accent\german.ftl", строка 90: accent-german-words-replace-42 = warnung
Не переведённая строка "accent-german-words-replace-43" в "accent\german.ftl", строка 92: accent-german-words-replace-43 = warnungen
Не переведённая строка "accent-german-words-replace-44" в "accent\german.ftl", строка 94: accent-german-words-replace-44 = und
Не переведённая строка "accent-german-words-replace-45" в "accent\german.ftl", строка 96: accent-german-words-replace-45 = karpfen
Не переведённая строка "accent-german-words-replace-46" в "accent\german.ftl", строка 98: accent-german-words-replace-46 = kommandant
Не переведённая строка "accent-german-words-replace-47" в "accent\german.ftl", строка 100: accent-german-words-replace-47 = bier
Не переведённая строка "accent-german-words-replace-48" в "accent\german.ftl", строка 102: accent-german-words-replace-48 = hallo
Не переведённая строка "accent-german-words-replace-49" в "accent\german.ftl", строка 104: accent-german-words-replace-49 = guten Tag
Не переведённая строка "accent-german-words-replace-50" в "accent\german.ftl", строка 106: accent-german-words-replace-50 = hilfe
Не переведённая строка "accent-german-words-replace-51" в "accent\german.ftl", строка 108: accent-german-words-replace-51 = auf Wiedersehen
Не переведённая строка "accent-german-words-replace-52" в "accent\german.ftl", строка 110: accent-german-words-replace-52 = tschüss
Не переведённая строка "accent-german-words-replace-53" в "accent\german.ftl", строка 113: accent-german-words-replace-53 = tschau
Не переведённая строка "accent-german-words-replace-54" в "accent\german.ftl", строка 115: accent-german-words-replace-54 = fantastisch
Не переведённая строка "accent-german-words-replace-55" в "accent\german.ftl", строка 117: accent-german-words-replace-55 = doppelgänger
Не переведённая строка "accent-german-words-replace-56" в "accent\german.ftl", строка 119: accent-german-words-replace-56 = verboten
Не переведённая строка "accent-german-words-replace-57" в "accent\german.ftl", строка 122: accent-german-words-replace-57 = schnell
Не переведённая строка "accent-german-words-replace-58" в "accent\german.ftl", строка 124: accent-german-words-replace-58 = krankenhaus
Не переведённая строка "accent-german-words-replace-59" в "accent\german.ftl", строка 126: accent-german-words-replace-59 = tesla coil
Не переведённая строка "accent-german-words-replace-60" в "accent\german.ftl", строка 128: accent-german-words-replace-60 = tesla coils
Не переведённая строка "accent-german-words-61" в "accent\german.ftl", строка 129: accent-german-words-61 = teslaloose
Не переведённая строка "accent-german-words-61-2" в "accent\german.ftl", строка 130: accent-german-words-61-2 = tesloose
Не переведённая строка "accent-german-words-61-3" в "accent\german.ftl", строка 131: accent-german-words-61-3 = lightning ball
Не переведённая строка "accent-german-words-replace-61" в "accent\german.ftl", строка 134: accent-german-words-replace-61 = kugelblitz

View file

@ -0,0 +1 @@
from fluentformatter import FluentFile, FluentFormatter

View file

@ -0,0 +1,230 @@
import os
import re
import sys
def is_english(text):
return bool(re.search('[a-zA-Z]', text))
def has_russian(text):
return bool(re.search('[а-яА-Я]', text))
def remove_braces_content(text):
text1 = re.sub(r'\{.*?\}', '', text)
text2 = re.sub(r'\[.*?\]', '', text1)
text3 = re.sub(r'\<.*?\>', '', text2)
return text3
def contains_ignored_word(text, ignore_list):
return any(ignored_word in text for ignored_word in ignore_list)
def check_translations(root_dir, report_file, ignore_list, ignore_files):
root_dir_abs = os.path.abspath(root_dir)
found_issues = False
localization_issues = []
for dirpath, _, filenames in os.walk(root_dir):
for filename in filenames:
if filename.endswith('.ftl'):
file_path = os.path.join(dirpath, filename)
rel_path = os.path.relpath(file_path, root_dir_abs)
# Проверка, должен ли файл быть игнорирован
if filename in ignore_files:
print(f'Игнорирование файла: {filename}')
continue
with open(file_path, 'r', encoding='utf-8') as ftl_file:
lines = ftl_file.readlines()
for line_num, line in enumerate(lines, start=1):
if '=' in line and not line.strip().startswith('#'):
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
value = remove_braces_content(value)
if not has_russian(value) and not contains_ignored_word(value, ignore_list):
if key.endswith('.desc') or key.endswith('.suffix'):
if is_english(value):
localization_issues.append(f'Не переведённая строка "{key}" в "{rel_path}", строка {line_num}: {line.strip()}\n')
elif is_english(value):
localization_issues.append(f'Не переведённая строка "{key}" в "{rel_path}", строка {line_num}: {line.strip()}\n')
if localization_issues:
found_issues = True
with open(report_file, 'a', encoding='utf-8') as report:
report.write("\nПроверка локализации:\n")
report.writelines(localization_issues)
return found_issues
def check_prototypes_for_russian(root_dir, report_file):
root_dir_abs = os.path.abspath(root_dir)
found_issues = False
prototype_issues = []
for dirpath, _, filenames in os.walk(root_dir):
for filename in filenames:
if filename.endswith('.yml') or filename.endswith('.yaml'):
file_path = os.path.join(dirpath, filename)
rel_path = os.path.relpath(file_path, root_dir_abs)
with open(file_path, 'r', encoding='utf-8') as prototype_file:
lines = prototype_file.readlines()
for line_num, line in enumerate(lines, start=1):
if has_russian(line):
prototype_issues.append(f'Строка с русскими символами в "{rel_path}", строка {line_num}: {line.strip()}\n')
if prototype_issues:
found_issues = True
with open(report_file, 'a', encoding='utf-8') as report:
report.write("\nПроверка прототипов:\n")
report.writelines(prototype_issues)
return found_issues
if __name__ == "__main__":
# Список слов или фраз, которые нужно игнорировать
ignore_list = [
'EntityUid',
'Zzz...',
'playglobalsound',
'variantize',
'ID',
'IP',
'TileY',
'TileX',
'X', 'Y',
'EI NATH',
'>MFW',
'Hello world!',
'green', 'red',
'purple',
'yellow',
'orange',
'$count',
'v1',
'Nanotrasen',
'float',
'$open',
'$rate',
'LV-426',
'LOOC',
'OOC',
'white',
'blue',
'grey',
'#1b487e',
'#B50F1D',
'Never gonna let you down!',
'Never gonna give you up!',
'power_net_query',
'zzz...',
'GENDER',
'$initialCount',
'Github',
'GitHub',
'Space Station 14',
'AHelp',
'TARCOL MINTI ZHERI',
"ONI'SOMA!",
'AIE KHUSE EU',
'Gray',
'$enabled',
'FOO-BAR-BAZ',
'$state',
'Float',
'Integer',
'left-4-zed',
'plant-B-gone',
'Pwr Game',
'sol dry',
'gray',
'UITest2',
'EVA',
'MK58',
'WT550',
'N1984',
'DEUS VULT',
'AMS-42',
'SAM-300',
'AJ-100',
'Deus vult! Ave maria!',
'TR-263',
'G-Man',
'RGB',
'Changelog',
'M1 Garand',
'BR-64',
'MG-42',
'MG-60',
'RPD',
"Monkin' Donuts",
'P-90',
'MP-38',
'MP5',
'MP7',
'PPSH-41',
'AKMS',
'AK74-U',
'G-36',
'M-28',
'AR-18',
'M16A4',
'STG 44',
'ACP-14',
'Glock-22',
'GL-79',
'M-41',
'DEAD-BEEF',
'UwU',
'C-4',
'Who ya gonna call?',
'Ue No',
'Carpe diem!',
'Space-Up!',
'Sun-kist',
'Robust Softdrinks',
'C-20r',
'Donut Corp.',
'LV426',
'Kept ya waiting, huh?',
'Getmore Chocolate Corp',
'Discount Dan',
'L6 SAW',
'L6C ROW',
'China Lake',
'Plant-B-Gone',
'Bon appétit!',
'Bon ap-petite!',
'Sol dry',
'hover entity', # Спорная хуйня, проверить в игре
'drag shadow', # Спорная хуйня, проверить в игре
'dbg_rotation1',
'dbg_rotation4',
'dbg_rotationTex',
'plague inc 2.0',
'False',
'True',
'chèvre'
] # Добавьте сюда слова, которые нужно игнорировать
# Список файлов, которые нужно игнорировать
ignore_files = ['italian.ftl', 'popup.ftl', 'controls.ftl', 'input.ftl', 'speech-chatsan.ftl', 'speech-liar.ftl', 'russian.ftl'] # Добавьте сюда имена файлов, которые нужно игнорировать
# Получаем директорию, где находится скрипт
script_dir = os.path.dirname(os.path.abspath(__file__))
# Устанавливаем корневую директорию и файл отчета
localization_directory = os.path.join(script_dir, '../../Resources/Locale/ru-RU')
prototypes_directory = os.path.join(script_dir, '../../Resources/Prototypes')
report_path = os.path.join(script_dir, 'Report.txt')
# Создаём/очищаем файл отчёта
open(report_path, 'w').close()
# Проверяем локализацию
localization_issues_found = check_translations(localization_directory, report_path, ignore_list, ignore_files)
# Проверяем прототипы на наличие русских символов
#prototypes_issues_found = check_prototypes_for_russian(prototypes_directory, report_path)
print(f"Отчет создан в {report_path}")

91
Tools/_sunrise/file.py Normal file
View file

@ -0,0 +1,91 @@
import typing
from fluent.syntax import ast
from yamlmodels import YAMLElements
import os
class File:
def __init__(self, full_path):
self.full_path = full_path
def read_data(self):
file = open(self.full_path, 'r', encoding='utf8')
# replace необходим для того, чтобы 1-е сообщение не считалось ast.Junk
file_data = file.read().replace('', '')
file.close()
return file_data
def save_data(self, file_data: typing.AnyStr):
os.makedirs(os.path.dirname(self.full_path), exist_ok=True)
file = open(self.full_path, 'w', encoding='utf8')
file.write(file_data)
file.close()
def get_relative_path(self, base_path):
return os.path.relpath(self.full_path, base_path)
def get_relative_path_without_extension(self, base_path):
return self.get_relative_path(base_path).split('.', maxsplit=1)[0]
def get_relative_parent_dir(self, base_path):
return os.path.relpath(self.get_parent_dir(), base_path)
def get_parent_dir(self):
return os.path.dirname(self.full_path)
def get_name(self):
return os.path.basename(self.full_path).split('.')[0]
class FluentFile(File):
def __init__(self, full_path):
super().__init__(full_path)
self.full_path = full_path
def parse_data(self, file_data: typing.AnyStr):
from fluent.syntax import FluentParser
return FluentParser().parse(file_data)
def serialize_data(self, parsed_file_data: ast.Resource):
from fluent.syntax import FluentSerializer
return FluentSerializer(with_junk=True).serialize(parsed_file_data)
def read_serialized_data(self):
return self.serialize_data(self.parse_data(self.read_data()))
def read_parsed_data(self):
return self.parse_data(self.read_data())
class YAMLFluentFileAdapter(File):
def __init__(self, full_path):
super().__init__(full_path)
self.full_path = full_path
# def create_fluent_from_yaml_elements(self, yaml_elements):
class YAMLFile(File):
def __init__(self, full_path):
super().__init__(full_path)
def parse_data(self, file_data: typing.AnyStr):
import yaml
return yaml.load(file_data, Loader=yaml.BaseLoader)
def get_elements(self, parsed_data):
if isinstance(parsed_data, list):
elements = YAMLElements(parsed_data).elements
# элемент может быть None, если имеет неизвестный тип
exist_elements = list(filter(lambda el: el, elements))
return exist_elements
return []

187
Tools/_sunrise/fluentast.py Normal file
View file

@ -0,0 +1,187 @@
import typing
from fluent.syntax import ast, FluentParser, FluentSerializer
from lokalisemodels import LokaliseKey
from pydash import py_
class FluentAstAbstract:
element = None
@classmethod
def get_id_name(cls, element):
if isinstance(element, ast.Junk):
return FluentAstJunk(element).get_id_name()
elif isinstance(element, ast.Message):
return FluentAstMessage(element).get_id_name()
elif isinstance(element, ast.Term):
return FluentAstTerm(element).get_id_name()
else:
return None
@classmethod
def create_element(cls, element):
if isinstance(element, ast.Junk):
cls.element = FluentAstJunk(element)
return cls.element
elif isinstance(element, ast.Message):
cls.element = FluentAstMessage(element)
return cls.element
elif isinstance(element, ast.Term):
cls.element = FluentAstTerm(element)
return cls.element
else:
return None
class FluentAstMessage:
def __init__(self, message: ast.Message):
self.message = message
self.element = message
def get_id_name(self):
return self.message.id.name
class FluentAstTerm:
def __init__(self, term: ast.Term):
self.term = term
self.element = term
def get_id_name(self):
return self.term.id.name
class FluentAstAttribute:
def __init__(self, id, value, parent_key = None):
self.id = id
self.value = value
self.parent_key = parent_key
class FluentAstAttributeFactory:
@classmethod
def from_yaml_element(cls, element):
attrs = []
if element.description:
attrs.append(FluentAstAttribute('desc', element.description))
if element.suffix:
attrs.append(FluentAstAttribute('suffix', element.suffix))
if not len(attrs):
return None
return attrs
class FluentAstJunk:
def __init__(self, junk: ast.Junk):
self.junk = junk
self.element = junk
def get_id_name(self):
return self.junk.content.split('=')[0].strip()
class FluentSerializedMessage:
@classmethod
def from_yaml_element(cls, id, value, attributes, parent_id = None, raw_key = False):
if not value and not id and not parent_id:
return None
if not attributes:
attributes = []
if len(list(filter(lambda attr: attr.id == 'desc', attributes))) == 0:
if parent_id:
attributes.append(FluentAstAttribute('desc', '{ ' + FluentSerializedMessage.get_key(parent_id) + '.desc' + ' }'));
else:
attributes.append(FluentAstAttribute('desc', '{ "" }'))
message = f'{cls.get_key(id, raw_key)} = {cls.get_value(value, parent_id)}\n'
if attributes and len(attributes):
full_message = message
for attr in attributes:
fluent_newlines = attr.value.replace("\n", "\n ");
full_message = cls.add_attr(full_message, attr.id, fluent_newlines, raw_key=raw_key)
desc_attr = py_.find(attributes, lambda a: a.id == 'desc')
if not desc_attr and parent_id:
full_message = cls.add_attr(full_message, 'desc', '{ ' + FluentSerializedMessage.get_key(parent_id) + '.desc' + ' }')
return full_message
return cls.to_serialized_message(message)
@classmethod
def from_lokalise_keys(cls, keys: typing.List[LokaliseKey]):
attributes_keys = list(filter(lambda k: k.is_attr, keys))
attributes = list(map(lambda k: FluentAstAttribute(id='.{name}'.format(name=k.get_key_last_name(k.key_name)),
value=FluentSerializedMessage.get_attr(k, k.get_key_last_name(k.key_name)), parent_key=k.get_parent_key()),
attributes_keys))
attributes_group = py_.group_by(attributes, 'parent_key')
serialized_message = ''
for key in keys:
if key.is_attr:
continue
key_name = key.get_key_last_name(key.key_name)
key_value = key.get_translation('ru').data['translation']
key_attributes = []
if len(attributes_group):
k = f'{key.get_key_base_name(key.key_name)}.{key_name}'
key_attributes = attributes_group[k] if k in attributes_group else []
message = key.serialize_message()
full_message = cls.from_yaml_element(key_name, key_value, key_attributes, key.get_parent_key(), True)
if full_message:
serialized_message = serialized_message + '\n' + full_message
elif message:
serialized_message = serialized_message + '\n' + message
else:
raise Exception('Что-то пошло не так')
return serialized_message
@staticmethod
def get_attr(k, name, parent_id = None):
if parent_id:
return "{ " + parent_id + f'.{name}' + " }"
else:
return k.get_translation('ru').data['translation']
@staticmethod
def to_serialized_message(string_message):
if not string_message:
return None
ast_message = FluentParser().parse(string_message)
serialized = FluentSerializer(with_junk=True).serialize(ast_message)
return serialized if serialized else ''
@staticmethod
def add_attr(message_str, attr_key, attr_value, raw_key = False):
prefix = '' if raw_key else '.'
return f'{message_str}\n {prefix}{attr_key} = {attr_value}'
@staticmethod
def get_value(value, parent_id):
if value:
return value
elif parent_id:
return '{ ' + FluentSerializedMessage.get_key(parent_id) + ' }'
else:
return '{ "" }'
@staticmethod
def get_key(id, raw = False):
if raw:
return f'{id}'
else:
return f'ent-{id}'

View file

@ -0,0 +1,100 @@
from fluent.syntax import ast
from fluentast import FluentAstAbstract
from pydash import py_
class FluentAstComparer:
def __init__(self, sourse_parsed: ast.Resource, target_parsed: ast.Resource):
self.sourse_parsed = sourse_parsed
self.target_parsed = target_parsed
self.source_elements = list(
filter(lambda el: el, list(map(lambda e: FluentAstAbstract.create_element(e), sourse_parsed.body))))
self.target_elements = list(
filter(lambda el: el, list(map(lambda e: FluentAstAbstract.create_element(e), target_parsed.body))))
# Возвращает полностью эквивалентные сообщения (не считая span)
def get_equal_elements(self):
comparator = lambda a, b: a.element.equals(b.element, ignored_fields=['span'])
return py_.intersection_with(self.source_elements, self.target_elements, comparator=comparator)
# Возвращает полностью неэквивалентные сообщения (не считая span)
def get_not_equal_elements(self):
comparator = lambda a, b: a.element.equals(b.element, ignored_fields=['span'])
diff = py_.difference_with(self.source_elements, self.target_elements, comparator=comparator)
return diff
# Возвращает сообщения с эквивалентными именами ключей
def get_equal_id_names(self):
comparator = lambda a, b: a.element.equals(b.element, ignored_fields=['span', 'value', 'comment', 'attributes'])
eq = py_.intersection_with(self.source_elements, self.target_elements, comparator=comparator)
return eq
# Возвращает сообщения с неэквивалентными именами ключей
def get_not_equal_id_names(self):
comparator = lambda a, b: a.element.equals(b.element, ignored_fields=['span', 'value', 'comment', 'attributes'])
diff = py_.difference_with(self.source_elements, self.target_elements, comparator=comparator)
return diff
# Возвращает сообщения target, существующие в source
def get_exist_id_names(self, source, target):
comparator = lambda a, b: a.element.equals(b.element, ignored_fields=['span', 'value', 'comment', 'attributes'])
eq = py_.intersection_with(source, target, comparator=comparator)
return eq
# Возвращает сообщения target, существующие в source
def get_not_exist_id_names(self):
comparator = lambda a, b: a.element.equals(b.element, ignored_fields=['span', 'value', 'comment', 'attributes'])
diff = py_.difference_with(self.target_elements, self.source_elements, comparator=comparator)
return diff
# Возвращает сообщения с эквивалентным значением и атрибутами
def get_equal_values_with_attrs(self):
comparator = lambda a, b: a.element.equals(b.element, ignored_fields=['span', 'id', 'comment'])
eq = py_.intersection_with(self.target_elements, self.source_elements, comparator=comparator)
return eq
# Возвращает сообщения из source с неэквивалентным значением и атрибутами
def get_not_equal_values_with_attrs(self):
comparator = lambda a, b: a.element.equals(b.element, ignored_fields=['span', 'id', 'comment'])
diff = py_.difference_with(self.source_elements, self.target_elements,
comparator=lambda a, b: a.element.equals(b.element,
ignored_fields=['span', 'id', 'comment']))
return diff
# Возвращает сообщения из source, существующие в target и source, с неэквивалентным значением и атрибутами
def get_not_equal_exist_values_with_attrs(self):
diff = py_.difference_with(self.source_elements, self.target_elements,
comparator=lambda a, b: a.element.equals(b.element,
ignored_fields=['span', 'id', 'comment']))
ex = self.get_exist_id_names(self.source_elements, self.target_elements)
exist = py_.intersection(diff, ex)
return exist
# Возвращает сообщения из target с неэквивалентным значением и атрибутами
def get_target_not_equal_values_with_attrs(self):
comparator = lambda a, b: a.element.equals(b.element, ignored_fields=['span', 'id', 'comment'])
diff = py_.difference_with(self.source_elements, self.target_elements, comparator=comparator)
return diff
# Возвращает сообщения, существующие в target и source, с неэквивалентным значением и атрибутами
def get_target_not_equal_exist_values_with_attrs(self):
diff = py_.difference_with(self.target_elements, self.source_elements,
comparator=lambda a, b: a.element.equals(b.element,
ignored_fields=['span', 'id', 'comment']))
exist = py_.intersection(diff, self.get_exist_id_names(self.target_elements, self.source_elements))
return exist
def find_message_by_id_name(self, id_name, list):
return py_.find(list, lambda el: el.get_id_name() == id_name)

View file

@ -0,0 +1,25 @@
from fluent.syntax import ast
from fluentast import FluentAstAbstract
class FluentAstManager:
def __init__(self, sourse_parsed: ast.Resource, target_parsed: ast.Resource):
self.sourse_parsed = sourse_parsed
self.target_parsed = target_parsed
self.source_elements = list(map(lambda e: FluentAstAbstract.create_element(e), sourse_parsed.body))
self.target_elements = list(map(lambda e: FluentAstAbstract.create_element(e), target_parsed.body))
def update_by_index(self, index, update_element: ast.Message):
source_element = None
try:
source_element = self.sourse_parsed.body[index]
except:
raise Exception(f'Нет элемента с индексом {index}')
if not source_element:
raise Exception(f'Элемен с индексом {index} не существует')
self.sourse_parsed.body[index] = update_element
return self.sourse_parsed

View file

@ -0,0 +1,37 @@
#!/usr/bin/env python3
# Форматтер, приводящий fluent-файлы (.ftl) в соответствие стайлгайду
# path - путь к папке, содержащий форматируемые файлы. Для форматирования всего проекта, необходимо заменить значение на root_dir_path
import typing
from file import FluentFile
from project import Project
from fluent.syntax import ast, FluentParser, FluentSerializer
######################################### Class defifitions ############################################################
class FluentFormatter:
@classmethod
def format(cls, fluent_files: typing.List[FluentFile]):
for file in fluent_files:
file_data = file.read_data()
parsed_file_data = file.parse_data(file_data)
serialized_file_data = file.serialize_data(parsed_file_data)
file.save_data(serialized_file_data)
@classmethod
def format_serialized_file_data(cls, file_data: typing.AnyStr):
parsed_data = FluentParser().parse(file_data)
return FluentSerializer(with_junk=True).serialize(parsed_data)
######################################## Var definitions ###############################################################
project = Project()
fluent_files = project.get_fluent_files_by_dir(project.ru_locale_dir_path)
########################################################################################################################
FluentFormatter.format(fluent_files)

2
Tools/_sunrise/key.env Normal file
View file

@ -0,0 +1,2 @@
lokalise_project_id=4706525766ab4ce5734b82.40263496
lokalise_personal_token=cccdf4f3add308b8af995039f5c4846494a6df49

220
Tools/_sunrise/keyfinder.py Normal file
View file

@ -0,0 +1,220 @@
import typing
import logging
from pydash import py_
from file import FluentFile
from fluentast import FluentAstAbstract
from fluentformatter import FluentFormatter
from project import Project
from fluent.syntax import ast, FluentParser, FluentSerializer
# Осуществляет актуализацию ключей. Находит файлы английского перевода, проверяет: есть ли русскоязычная пара
# Если нет - создаёт файл с копией переводов из англоязычного
# Далее, пофайлово проверяются ключи. Если в английском файле больше ключей - создает недостающие в русском, с английской копией перевода
# Отмечает русские файлы, в которых есть те ключи, что нет в аналогичных английских
# Отмечает русские файлы, у которых нет англоязычной пары
######################################### Class defifitions ############################################################
class RelativeFile:
def __init__(self, file: FluentFile, locale: typing.AnyStr, relative_path_from_locale: typing.AnyStr):
self.file = file
self.locale = locale
self.relative_path_from_locale = relative_path_from_locale
class FilesFinder:
def __init__(self, project: Project):
self.project: Project = project
self.created_files: typing.List[FluentFile] = []
def get_relative_path_dict(self, file: FluentFile, locale):
if locale == 'ru-RU':
return RelativeFile(file=file, locale=locale,
relative_path_from_locale=file.get_relative_path(self.project.ru_locale_dir_path))
elif locale == 'en-US':
return RelativeFile(file=file, locale=locale,
relative_path_from_locale=file.get_relative_path(self.project.en_locale_dir_path))
else:
raise Exception(f'Локаль {locale} не поддерживается')
def get_file_pair(self, en_file: FluentFile) -> typing.Tuple[FluentFile, FluentFile]:
ru_file_path = en_file.full_path.replace('en-US', 'ru-RU')
ru_file = FluentFile(ru_file_path)
return en_file, ru_file
def execute(self):
self.created_files = []
groups = self.get_files_pars()
keys_without_pair = list(filter(lambda g: len(groups[g]) < 2, groups))
for key_without_pair in keys_without_pair:
relative_file: RelativeFile = groups.get(key_without_pair)[0]
if relative_file.locale == 'en-US':
ru_file = self.create_ru_analog(relative_file)
self.created_files.append(ru_file)
elif relative_file.locale == 'ru-RU':
is_engine_files = "robust-toolbox" in (relative_file.file.full_path)
is_corvax_files = "corvax" in (relative_file.file.full_path)
if not is_engine_files and not is_corvax_files:
self.warn_en_analog_not_exist(relative_file)
else:
raise Exception(f'Файл {relative_file.file.full_path} имеет неизвестную локаль "{relative_file.locale}"')
return self.created_files
def get_files_pars(self):
en_fluent_files = self.project.get_fluent_files_by_dir(project.en_locale_dir_path)
ru_fluent_files = self.project.get_fluent_files_by_dir(project.ru_locale_dir_path)
en_fluent_relative_files = list(map(lambda f: self.get_relative_path_dict(f, 'en-US'), en_fluent_files))
ru_fluent_relative_files = list(map(lambda f: self.get_relative_path_dict(f, 'ru-RU'), ru_fluent_files))
relative_files = py_.flatten_depth(py_.concat(en_fluent_relative_files, ru_fluent_relative_files), depth=1)
return py_.group_by(relative_files, 'relative_path_from_locale')
def create_ru_analog(self, en_relative_file: RelativeFile) -> FluentFile:
en_file: FluentFile = en_relative_file.file
en_file_data = en_file.read_data()
ru_file_path = en_file.full_path.replace('en-US', 'ru-RU')
ru_file = FluentFile(ru_file_path)
ru_file.save_data(en_file_data)
logging.info(f'Создан файл {ru_file_path} с переводами из английского файла')
return ru_file
def warn_en_analog_not_exist(self, ru_relative_file: RelativeFile):
file: FluentFile = ru_relative_file.file
en_file_path = file.full_path.replace('ru-RU', 'en-US')
logging.warning(f'Файл {file.full_path} не имеет английского аналога по пути {en_file_path}')
class KeyFinder:
def __init__(self, files_dict):
self.files_dict = files_dict
self.changed_files: typing.List[FluentFile] = []
def execute(self) -> typing.List[FluentFile]:
self.changed_files = []
for pair in self.files_dict:
ru_relative_file = py_.find(self.files_dict[pair], {'locale': 'ru-RU'})
en_relative_file = py_.find(self.files_dict[pair], {'locale': 'en-US'})
if not en_relative_file or not ru_relative_file:
continue
ru_file: FluentFile = ru_relative_file.file
en_file: FluentFile = en_relative_file.file
self.compare_files(en_file, ru_file)
return self.changed_files
def compare_files(self, en_file, ru_file):
ru_file_parsed: ast.Resource = ru_file.parse_data(ru_file.read_data())
en_file_parsed: ast.Resource = en_file.parse_data(en_file.read_data())
self.write_to_ru_files(ru_file, ru_file_parsed, en_file_parsed)
self.log_not_exist_en_files(en_file, ru_file_parsed, en_file_parsed)
def write_to_ru_files(self, ru_file, ru_file_parsed, en_file_parsed):
for idx, en_message in enumerate(en_file_parsed.body):
if isinstance(en_message, ast.ResourceComment) or isinstance(en_message, ast.GroupComment) or isinstance(en_message, ast.Comment):
continue
ru_message_analog_idx = py_.find_index(ru_file_parsed.body, lambda ru_message: self.find_duplicate_message_id_name(ru_message, en_message))
have_changes = False
# Attributes
if getattr(en_message, 'attributes', None) and ru_message_analog_idx != -1:
if not ru_file_parsed.body[ru_message_analog_idx].attributes:
ru_file_parsed.body[ru_message_analog_idx].attributes = en_message.attributes
have_changes = True
else:
for en_attr in en_message.attributes:
ru_attr_analog = py_.find(ru_file_parsed.body[ru_message_analog_idx].attributes, lambda ru_attr: ru_attr.id.name == en_attr.id.name)
if not ru_attr_analog:
ru_file_parsed.body[ru_message_analog_idx].attributes.append(en_attr)
have_changes = True
# New elements
if ru_message_analog_idx == -1:
ru_file_body = ru_file_parsed.body
if (len(ru_file_body) >= idx + 1):
ru_file_parsed = self.append_message(ru_file_parsed, en_message, idx)
else:
ru_file_parsed = self.push_message(ru_file_parsed, en_message)
have_changes = True
if have_changes:
serialized = serializer.serialize(ru_file_parsed)
self.save_and_log_file(ru_file, serialized, en_message)
def log_not_exist_en_files(self, en_file, ru_file_parsed, en_file_parsed):
for idx, ru_message in enumerate(ru_file_parsed.body):
if isinstance(ru_message, ast.ResourceComment) or isinstance(ru_message, ast.GroupComment) or isinstance(ru_message, ast.Comment):
continue
en_message_analog = py_.find(en_file_parsed.body, lambda en_message: self.find_duplicate_message_id_name(ru_message, en_message))
if not en_message_analog:
logging.warning(f'Ключ "{FluentAstAbstract.get_id_name(ru_message)}" не имеет английского аналога по пути {en_file.full_path}"')
def append_message(self, ru_file_parsed, en_message, en_message_idx):
ru_message_part_1 = ru_file_parsed.body[0:en_message_idx]
ru_message_part_middle = [en_message]
ru_message_part_2 = ru_file_parsed.body[en_message_idx:]
new_body = py_.flatten_depth([ru_message_part_1, ru_message_part_middle, ru_message_part_2], depth=1)
ru_file_parsed.body = new_body
return ru_file_parsed
def push_message(self, ru_file_parsed, en_message):
ru_file_parsed.body.append(en_message)
return ru_file_parsed
def save_and_log_file(self, file, file_data, message):
file.save_data(file_data)
logging.info(f'В файл {file.full_path} добавлен ключ "{FluentAstAbstract.get_id_name(message)}"')
self.changed_files.append(file)
def find_duplicate_message_id_name(self, ru_message, en_message):
ru_element_id_name = FluentAstAbstract.get_id_name(ru_message)
en_element_id_name = FluentAstAbstract.get_id_name(en_message)
if not ru_element_id_name or not en_element_id_name:
return False
if ru_element_id_name == en_element_id_name:
return ru_message
else:
return None
######################################## Var definitions ###############################################################
logging.basicConfig(level = logging.INFO)
project = Project()
parser = FluentParser()
serializer = FluentSerializer(with_junk=True)
files_finder = FilesFinder(project)
key_finder = KeyFinder(files_finder.get_files_pars())
########################################################################################################################
print('Проверка актуальности файлов ...')
created_files = files_finder.execute()
if len(created_files):
print('Форматирование созданных файлов ...')
FluentFormatter.format(created_files)
print('Проверка актуальности ключей ...')
changed_files = key_finder.execute()
if len(changed_files):
print('Форматирование изменённых файлов ...')
FluentFormatter.format(changed_files)

View file

@ -0,0 +1,48 @@
from fluent.syntax import ast
from fluentast import FluentAstMessage
from fluentastcomparer import FluentAstComparer
from fluentastmanager import FluentAstManager
class LokaliseFluentAstComparerManager:
def __init__(self, sourse_parsed: ast.Resource, target_parsed: ast.Resource):
self.sourse_parsed = sourse_parsed
self.target_parsed = target_parsed
self.comparer = FluentAstComparer(sourse_parsed, target_parsed)
self.ast_manager = FluentAstManager(sourse_parsed, target_parsed)
def for_update(self):
for_update = self.comparer.get_not_equal_exist_values_with_attrs()
if not len(for_update):
return []
return for_update
def update(self, for_update):
for update in for_update:
idx = self.comparer.sourse_parsed.body.index(update.element)
update_mess: FluentAstMessage = self.comparer.find_message_by_id_name(update.get_id_name(),
self.comparer.target_elements)
self.ast_manager.update_by_index(idx, update_mess.element)
return self.ast_manager.sourse_parsed
def for_delete(self):
for_delete = self.comparer.get_not_exist_id_names()
if len(for_delete):
keys = list(map(lambda el: el.get_id_name(), for_delete))
print(f'Следующие ключи есть в lokalise, но нет в файле. Возможно, их нужно удалить из lokalise: {keys}')
return for_delete
def for_create(self):
for_create = self.comparer.get_not_equal_id_names()
if len(for_create):
keys = list(map(lambda el: el.get_id_name(), for_create))
print(f'Следующих ключей файла нет в lokalise. Необходимо добавить: {keys}')
return for_create

View file

@ -0,0 +1,33 @@
import lokalise
import typing
from lokalisemodels import LokaliseKey
from pydash import py_
class LokaliseProject:
def __init__(self, project_id, personal_token):
self.project_id = "4706525766ab4ce5734b82.40263496"
self.personal_token = "cccdf4f3add308b8af995039f5c4846494a6df49"
self.client = lokalise.Client(self.personal_token)
def get_all_keys(self) -> typing.List[LokaliseKey]:
page = 1
keys = self.get_keys(page=page)
keys_items: typing.List[lokalise.client.KeyModel] = []
general_count = 0
while (general_count < keys.total_count):
general_count = general_count + len(keys.items)
keys_items = py_.flatten_depth(py_.concat(keys_items, keys.items), depth=1)
if (general_count == keys.total_count):
break
next_page = page = page + 1
keys = self.get_keys(page=next_page)
sorted_list = py_.sort(keys_items, key=lambda item: item.translations_modified_at_timestamp, reverse=True)
return list(map(lambda k: LokaliseKey(k), sorted_list))
def get_keys(self, page):
return self.client.keys(self.project_id, {'page': page, 'limit': 5000, 'include_translations': 1})

View file

@ -0,0 +1,64 @@
import typing
import os
from pydash import py_
from project import Project
class LocalePath:
def __init__(self, relative_file_path):
self.ru = os.path.join(Project().ru_locale_dir_path, relative_file_path)
self.en = os.path.join(Project().en_locale_dir_path, relative_file_path)
class LokaliseTranslation:
def __init__(self, data, key_name: typing.AnyStr):
self.key_name = key_name,
self.data = data
class LokaliseKey:
def __init__(self, data):
self.data = data
self.key_name = self.data.key_name['web']
self.key_base_name = self.get_key_base_name(self.key_name)
self.is_attr = self.check_is_attr()
def get_file_path(self):
relative_dir_path = '{relative_file_path}.ftl'.format(
relative_file_path='/'.join(self.data.key_name['web'].split('.')[0].split('::')))
return LocalePath(relative_dir_path)
def get_key_base_name(self, key_name):
splitted_name = key_name.split('.')
return splitted_name[0]
def get_key_last_name(self, key_name):
splitted_name = key_name.split('.')
return py_.last(splitted_name)
def get_parent_key(self):
if self.is_attr:
splitted_name = self.key_name.split('.')[0:-1]
return '.'.join(splitted_name)
else:
return None
def check_is_attr(self):
return len(self.key_name.split('.')) > 2
def serialize(self):
if self.is_attr:
return self.serialize_attr()
else:
return self.serialize_message()
def serialize_attr(self):
return '.{name} = {value}'.format(name=self.get_key_last_name(self.key_name), value=self.get_translation('ru').data['translation'])
def serialize_message(self):
return '{name} = {value}'.format(name=self.get_key_last_name(self.key_name), value=self.get_translation('ru').data['translation'])
def get_translation(self, language_iso='ru'):
return list(map(lambda data: LokaliseTranslation(key_name=self.data.key_name['web'], data=data), py_.filter(self.data.translations, {'language_iso': language_iso})))[0]

31
Tools/_sunrise/project.py Normal file
View file

@ -0,0 +1,31 @@
import pathlib
import os
import glob
from file import FluentFile
class Project:
def __init__(self):
self.base_dir_path = pathlib.Path(os.path.abspath(os.curdir)).parent.parent.resolve()
self.resources_dir_path = os.path.join(self.base_dir_path, 'Resources')
self.locales_dir_path = os.path.join(self.resources_dir_path, 'Locale')
self.ru_locale_dir_path = os.path.join(self.locales_dir_path, 'ru-RU')
self.en_locale_dir_path = os.path.join(self.locales_dir_path, 'en-US')
self.prototypes_dir_path = os.path.join(self.resources_dir_path, "Prototypes")
self.en_locale_prototypes_dir_path = os.path.join(self.en_locale_dir_path, 'ss14-ru', 'prototypes')
self.ru_locale_prototypes_dir_path = os.path.join(self.ru_locale_dir_path, 'ss14-ru', 'prototypes')
def get_files_paths_by_dir(self, dir_path, files_extenstion):
return glob.glob(f'{dir_path}/**/*.{files_extenstion}', recursive=True)
def get_fluent_files_by_dir(self, dir_path):
files = []
files_paths_list = glob.glob(f'{dir_path}/**/*.ftl', recursive=True)
for file_path in files_paths_list:
try:
files.append(FluentFile(file_path))
except:
continue
return files

Binary file not shown.

View file

@ -0,0 +1,67 @@
import logging
import typing
from fluent.syntax import FluentParser, FluentSerializer
from pydash import py_
from file import FluentFile
from fluentast import FluentSerializedMessage
from lokalise_fluent_ast_comparer_manager import LokaliseFluentAstComparerManager
from lokalise_project import LokaliseProject
from lokalisemodels import LokaliseKey
import os
######################################### Class defifitions ############################################################
# TODO непереведенные элементы приходят как { "" }. Необходимо сохранять английский перевод
class TranslationsAssembler:
def __init__(self, items: typing.List[LokaliseKey]):
self.group = py_.group_by(items, 'key_base_name')
keys = list(self.group.keys())
self.sorted_keys = py_.sort_by(keys, lambda key: self.sort_by_translations_timestamp(self.group[key]),
reverse=True)
def execute(self):
for keys in self.group:
full_message = FluentSerializedMessage.from_lokalise_keys(self.group[keys])
parsed_message = FluentParser().parse(full_message)
ru_full_path = self.group[keys][0].get_file_path().ru
ru_file = FluentFile(ru_full_path)
try:
ru_file_parsed = ru_file.read_parsed_data()
except:
logging.error(f'Файла {ru_file.full_path} не существует')
continue
manager = LokaliseFluentAstComparerManager(sourse_parsed=ru_file_parsed, target_parsed=parsed_message)
for_update = manager.for_update()
for_create = manager.for_create()
for_delete = manager.for_delete()
if len(for_update):
updated_ru_file_parsed = manager.update(for_update)
updated_ru_file_serialized = FluentSerializer(with_junk=True).serialize(updated_ru_file_parsed)
ru_file.save_data(updated_ru_file_serialized)
updated_keys = list(map(lambda el: el.get_id_name(), for_update))
logging.info(f'Обновлены ключи: {updated_keys} в файле {ru_file.full_path}')
def sort_by_translations_timestamp(self, list):
sorted_list = py_.sort_by(list, 'data.translations_modified_at_timestamp', reverse=True)
return sorted_list[0].data.translations_modified_at_timestamp
######################################## Var definitions ###############################################################
logging.basicConfig(level=logging.INFO)
lokalise_project_id = os.getenv('lokalise_project_id')
lokalise_personal_token = os.getenv('lokalise_personal_token')
lokalise_project = LokaliseProject(project_id=lokalise_project_id,
personal_token=lokalise_personal_token)
all_keys: typing.List[LokaliseKey] = lokalise_project.get_all_keys()
translations_assembler = TranslationsAssembler(all_keys)
########################################################################################################################
translations_assembler.execute()

View file

@ -0,0 +1,107 @@
import os
from fluent.syntax.parser import FluentParser
from fluent.syntax.serializer import FluentSerializer
from file import YAMLFile, FluentFile
from fluentast import FluentSerializedMessage, FluentAstAttributeFactory
from fluentformatter import FluentFormatter
from project import Project
import logging
######################################### Class defifitions ############################################################
class YAMLExtractor:
def __init__(self, yaml_files):
self.yaml_files = yaml_files
def execute(self):
for yaml_file in yaml_files:
yaml_elements = yaml_file.get_elements(yaml_file.parse_data(yaml_file.read_data()))
if not len(yaml_elements):
continue
fluent_file_serialized = self.get_serialized_fluent_from_yaml_elements(yaml_elements)
if not fluent_file_serialized:
continue
pretty_fluent_file_serialized = formatter.format_serialized_file_data(fluent_file_serialized)
relative_parent_dir = yaml_file.get_relative_parent_dir(project.prototypes_dir_path).lower()
file_name = yaml_file.get_name()
en_fluent_file_path = self.create_en_fluent_file(relative_parent_dir, file_name, pretty_fluent_file_serialized)
ru_fluent_file_path = self.create_ru_fluent_file(en_fluent_file_path)
def get_serialized_fluent_from_yaml_elements(self, yaml_elements):
fluent_serialized_messages = list(
map(lambda el: FluentSerializedMessage.from_yaml_element(el.id, el.name, FluentAstAttributeFactory.from_yaml_element(el), el.parent_id), yaml_elements)
)
fluent_exist_serialized_messages = list(filter(lambda m: m, fluent_serialized_messages))
if not len(fluent_exist_serialized_messages):
return None
return '\n'.join(fluent_exist_serialized_messages)
def create_en_fluent_file(self, relative_parent_dir, file_name, file_data):
# Проверяем, если информация берется из директории _Sunrise
if '_sunrise' in relative_parent_dir:
# Создаем путь для сохранения в en-US/_sunrise/_prototypes
en_new_dir_path = os.path.join(project.en_locale_dir_path, '_sunrise', '_prototypes', relative_parent_dir.replace('_sunrise', '').strip(os.sep))
else:
# Обычный путь сохранения
en_new_dir_path = os.path.join(project.en_locale_prototypes_dir_path, relative_parent_dir)
# Убедимся, что директория существует
os.makedirs(en_new_dir_path, exist_ok=True)
# Формируем полный путь к файлу
en_fluent_file_path = os.path.join(en_new_dir_path, f'{file_name}.ftl')
# Сохраняем файл
en_fluent_file = FluentFile(en_fluent_file_path)
en_fluent_file.save_data(file_data)
logging.info(f'Актуализирован файл английской локализации {en_fluent_file.full_path}')
return en_fluent_file.full_path
def create_ru_fluent_file(self, en_analog_file_path):
if '_sunrise' in en_analog_file_path.lower():
# Если уже есть _sunrise, просто заменяем en-US на ru-RU
ru_file_full_path = en_analog_file_path.replace('en-US', 'ru-RU')
else:
# Обычный путь сохранения
ru_file_full_path = en_analog_file_path.replace('en-US', 'ru-RU')
# Убедимся, что директория существует
os.makedirs(os.path.dirname(ru_file_full_path), exist_ok=True)
if os.path.isfile(ru_file_full_path):
return
else:
en_file = FluentFile(f'{en_analog_file_path}')
file = FluentFile(f'{ru_file_full_path}')
file.save_data(en_file.read_data())
logging.info(f'Создан файл русской локализации {ru_file_full_path}')
return ru_file_full_path
######################################## Var definitions ###############################################################
logging.basicConfig(level = logging.INFO)
project = Project()
serializer = FluentSerializer()
parser = FluentParser()
formatter = FluentFormatter()
yaml_files_paths = project.get_files_paths_by_dir(project.prototypes_dir_path, 'yml')
yaml_files = list(map(lambda yaml_file_path: YAMLFile(yaml_file_path), yaml_files_paths))
########################################################################################################################
logging.info(f'Поиск yaml-файлов ...')
YAMLExtractor(yaml_files).execute()

View file

@ -0,0 +1,26 @@
class YAMLEntity:
def __init__(self, id, name, description, suffix, parent_id = None):
self.id = id
self.name = name
self.description = description
self.suffix = suffix
self.parent_id = parent_id
class YAMLElements:
def __init__(self, items):
self.elements = list(map(lambda i: self.create_element(i), items))
def create_element(self, item):
if not 'id' in item:
return None
if item['type'] == 'entity':
entity = YAMLEntity(item['id'], item['name'] if 'name' in item else None,
item['description'] if 'description' in item else None,
item['suffix'] if 'suffix' in item else None,
item['parent'] if 'parent' in item else None
)
return entity
else:
return None