Translation key extractor and code generator (#160)

Generates a new `translation_key.rs` with all bundled translations.
Closes #158.
This commit is contained in:
Sandro Marques 2022-11-27 13:12:08 +00:00 committed by GitHub
parent 86be031a31
commit 6437381339
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 21866 additions and 19 deletions

View file

@ -106,15 +106,19 @@ impl Config for Game {
client.send_message("\nTranslated Text");
client.send_message(
" - 'chat.type.advancement.task': ".into_text()
+ Text::translate("chat.type.advancement.task", []),
+ Text::translate(translation_key::CHAT_TYPE_ADVANCEMENT_TASK, []),
);
client.send_message(
" - 'chat.type.advancement.task' with slots: ".into_text()
+ Text::translate(
"chat.type.advancement.task",
translation_key::CHAT_TYPE_ADVANCEMENT_TASK,
["arg1".into(), "arg2".into()],
),
);
client.send_message(
" - 'custom.translation_key': ".into_text()
+ Text::translate("custom.translation_key", []),
);
// Scoreboard value example
client.send_message("\nScoreboard Values");

File diff suppressed because it is too large Load diff

View file

@ -37,7 +37,15 @@ public class Main implements ModInitializer {
public void onInitialize() {
LOGGER.info("Starting extractors...");
var extractors = new Extractor[]{new Blocks(), new Entities(), new EntityData(), new Packets(), new Items(), new Enchants()};
var extractors = new Extractor[]{
new Blocks(),
new Enchants(),
new Entities(),
new EntityData(),
new Items(),
new Packets(),
new TranslationKeys(),
};
Path outputDirectory;
try {

View file

@ -0,0 +1,56 @@
package rs.valence.extractor.extractors;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.minecraft.util.Language;
import rs.valence.extractor.Main;
import java.lang.reflect.Field;
import java.util.Map;
public class TranslationKeys implements Main.Extractor {
@Override
public String fileName() {
return "translation_keys.json";
}
@Override
public JsonElement extract() {
JsonArray translationsJson = new JsonArray();
Map<String, String> translations = extractTranslations();
for (var translation : translations.entrySet()) {
String translationKey = translation.getKey();
String translationValue = translation.getValue();
var translationJson = new JsonObject();
translationJson.addProperty("key", translationKey);
translationJson.addProperty("english_translation", translationValue);
translationsJson.add(translationJson);
}
return translationsJson;
}
@SuppressWarnings("unchecked")
private static Map<String, String> extractTranslations() {
Language language = Language.getInstance();
Class<? extends Language> anonymousClass = language.getClass();
for (Field field : anonymousClass.getDeclaredFields()) {
try {
Object fieldValue = field.get(language);
if (fieldValue instanceof Map<?, ?>) {
return (Map<String, String>) fieldValue;
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed reflection on field '" + field + "' on class '" + anonymousClass + "'", e);
}
}
throw new RuntimeException("Did not find anonymous map under 'net.minecraft.util.Language.create()'");
}
}

View file

@ -147,8 +147,8 @@ pub mod prelude {
pub use valence_protocol::text::Color;
pub use valence_protocol::types::{GameMode, Hand, SoundCategory};
pub use valence_protocol::{
ident, BlockKind, BlockPos, BlockState, Ident, ItemKind, ItemStack, Text, TextFormat,
Username, MINECRAFT_VERSION, PROTOCOL_VERSION,
ident, translation_key, BlockKind, BlockPos, BlockState, Ident, ItemKind, ItemStack, Text,
TextFormat, Username, MINECRAFT_VERSION, PROTOCOL_VERSION,
};
pub use vek::{Aabb, Mat2, Mat3, Mat4, Vec2, Vec3, Vec4};
pub use world::{World, WorldId, WorldMeta, Worlds};

View file

@ -20,7 +20,7 @@ use valence_protocol::packets::s2c::login::{
DisconnectLogin, EncryptionRequest, LoginPluginRequest,
};
use valence_protocol::types::{MsgSigOrVerifyToken, SignedProperty, SignedPropertyOwned};
use valence_protocol::{Decode, Ident, RawBytes, Text, Username, VarInt};
use valence_protocol::{translation_key, Decode, Ident, RawBytes, Text, Username, VarInt};
use crate::config::Config;
use crate::player_textures::SignedPlayerTextures;
@ -95,7 +95,10 @@ pub(super) async fn online(
match resp.status() {
StatusCode::OK => {}
StatusCode::NO_CONTENT => {
let reason = Text::translate("multiplayer.disconnect.unverified_username", []);
let reason = Text::translate(
translation_key::MULTIPLAYER_DISCONNECT_UNVERIFIED_USERNAME,
[],
);
ctrl.send_packet(&DisconnectLogin { reason }).await?;
bail!("session server could not verify username");
}

View file

@ -8,14 +8,16 @@ use proc_macro2::{Ident, Span};
mod block;
mod enchant;
mod item;
mod translation_key;
pub fn main() -> anyhow::Result<()> {
println!("cargo:rerun-if-changed=../extracted/");
let generators = [
(block::build as fn() -> _, "block.rs"),
(item::build, "item.rs"),
(enchant::build, "enchant.rs"),
(item::build, "item.rs"),
(translation_key::build, "translation_key.rs"),
];
let out_dir = env::var_os("OUT_DIR").context("failed to get OUT_DIR env var")?;

View file

@ -0,0 +1,43 @@
use anyhow::Ok;
use heck::ToShoutySnakeCase;
use proc_macro2::TokenStream;
use quote::quote;
use serde::Deserialize;
use crate::ident;
#[derive(Deserialize, Clone, Debug)]
struct Translation {
key: String,
english_translation: String,
}
/// Escapes characters that have special meaning inside docs.
fn escape(text: &str) -> String {
text.replace('[', "\\[").replace(']', "\\]")
}
pub fn build() -> anyhow::Result<TokenStream> {
let translations = serde_json::from_str::<Vec<Translation>>(include_str!(
"../../extracted/translation_keys.json"
))?;
let translation_key_consts = translations
.iter()
.map(|translation| {
let const_id = ident(translation.key.to_shouty_snake_case());
let key = &translation.key;
let english_translation = &translation.english_translation;
let doc = escape(english_translation);
quote! {
#[doc = #doc]
pub const #const_id: &str = #key;
}
})
.collect::<Vec<TokenStream>>();
Ok(quote! {
#(#translation_key_consts)*
})
}

View file

@ -112,6 +112,7 @@ mod item;
pub mod packets;
mod raw_bytes;
pub mod text;
pub mod translation_key;
pub mod types;
pub mod username;
mod var_int;

View file

@ -885,23 +885,18 @@ mod tests {
assert_eq!(color_from_str("blue"), Some(Color::BLUE));
}
#[test]
fn text_empty() {
assert!("".into_text().is_empty());
let txt = "".into_text() + Text::translate("", []) + ("".italic().color(Color::RED) + "");
assert!(txt.is_empty());
assert!(txt.to_string().is_empty());
}
#[test]
fn translate() {
let txt = Text::translate("key", ["arg1".into(), "arg2".into()]);
let txt = Text::translate(
valence_protocol::translation_key::CHAT_TYPE_ADVANCEMENT_TASK,
["arg1".into(), "arg2".into()],
);
let serialized = serde_json::to_string(&txt).unwrap();
let deserialized: Text = serde_json::from_str(&serialized).unwrap();
assert_eq!(
serialized,
"{\"translate\":\"key\",\"with\":[{\"text\":\"arg1\"},{\"text\":\"arg2\"}]}"
"{\"translate\":\"chat.type.advancement.task\",\"with\":[{\"text\":\"arg1\"},{\"text\"\
:\"arg2\"}]}"
);
assert_eq!(txt, deserialized);
}

View file

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/translation_key.rs"));