mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 14:31:30 +11:00
Translation key extractor and code generator (#160)
Generates a new `translation_key.rs` with all bundled translations. Closes #158.
This commit is contained in:
parent
86be031a31
commit
6437381339
|
@ -106,15 +106,19 @@ impl Config for Game {
|
||||||
client.send_message("\nTranslated Text");
|
client.send_message("\nTranslated Text");
|
||||||
client.send_message(
|
client.send_message(
|
||||||
" - 'chat.type.advancement.task': ".into_text()
|
" - 'chat.type.advancement.task': ".into_text()
|
||||||
+ Text::translate("chat.type.advancement.task", []),
|
+ Text::translate(translation_key::CHAT_TYPE_ADVANCEMENT_TASK, []),
|
||||||
);
|
);
|
||||||
client.send_message(
|
client.send_message(
|
||||||
" - 'chat.type.advancement.task' with slots: ".into_text()
|
" - 'chat.type.advancement.task' with slots: ".into_text()
|
||||||
+ Text::translate(
|
+ Text::translate(
|
||||||
"chat.type.advancement.task",
|
translation_key::CHAT_TYPE_ADVANCEMENT_TASK,
|
||||||
["arg1".into(), "arg2".into()],
|
["arg1".into(), "arg2".into()],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
client.send_message(
|
||||||
|
" - 'custom.translation_key': ".into_text()
|
||||||
|
+ Text::translate("custom.translation_key", []),
|
||||||
|
);
|
||||||
|
|
||||||
// Scoreboard value example
|
// Scoreboard value example
|
||||||
client.send_message("\nScoreboard Values");
|
client.send_message("\nScoreboard Values");
|
||||||
|
|
21734
extracted/translation_keys.json
Normal file
21734
extracted/translation_keys.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -37,7 +37,15 @@ public class Main implements ModInitializer {
|
||||||
public void onInitialize() {
|
public void onInitialize() {
|
||||||
LOGGER.info("Starting extractors...");
|
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;
|
Path outputDirectory;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -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()'");
|
||||||
|
}
|
||||||
|
}
|
|
@ -147,8 +147,8 @@ pub mod prelude {
|
||||||
pub use valence_protocol::text::Color;
|
pub use valence_protocol::text::Color;
|
||||||
pub use valence_protocol::types::{GameMode, Hand, SoundCategory};
|
pub use valence_protocol::types::{GameMode, Hand, SoundCategory};
|
||||||
pub use valence_protocol::{
|
pub use valence_protocol::{
|
||||||
ident, BlockKind, BlockPos, BlockState, Ident, ItemKind, ItemStack, Text, TextFormat,
|
ident, translation_key, BlockKind, BlockPos, BlockState, Ident, ItemKind, ItemStack, Text,
|
||||||
Username, MINECRAFT_VERSION, PROTOCOL_VERSION,
|
TextFormat, Username, MINECRAFT_VERSION, PROTOCOL_VERSION,
|
||||||
};
|
};
|
||||||
pub use vek::{Aabb, Mat2, Mat3, Mat4, Vec2, Vec3, Vec4};
|
pub use vek::{Aabb, Mat2, Mat3, Mat4, Vec2, Vec3, Vec4};
|
||||||
pub use world::{World, WorldId, WorldMeta, Worlds};
|
pub use world::{World, WorldId, WorldMeta, Worlds};
|
||||||
|
|
|
@ -20,7 +20,7 @@ use valence_protocol::packets::s2c::login::{
|
||||||
DisconnectLogin, EncryptionRequest, LoginPluginRequest,
|
DisconnectLogin, EncryptionRequest, LoginPluginRequest,
|
||||||
};
|
};
|
||||||
use valence_protocol::types::{MsgSigOrVerifyToken, SignedProperty, SignedPropertyOwned};
|
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::config::Config;
|
||||||
use crate::player_textures::SignedPlayerTextures;
|
use crate::player_textures::SignedPlayerTextures;
|
||||||
|
@ -95,7 +95,10 @@ pub(super) async fn online(
|
||||||
match resp.status() {
|
match resp.status() {
|
||||||
StatusCode::OK => {}
|
StatusCode::OK => {}
|
||||||
StatusCode::NO_CONTENT => {
|
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?;
|
ctrl.send_packet(&DisconnectLogin { reason }).await?;
|
||||||
bail!("session server could not verify username");
|
bail!("session server could not verify username");
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,16 @@ use proc_macro2::{Ident, Span};
|
||||||
mod block;
|
mod block;
|
||||||
mod enchant;
|
mod enchant;
|
||||||
mod item;
|
mod item;
|
||||||
|
mod translation_key;
|
||||||
|
|
||||||
pub fn main() -> anyhow::Result<()> {
|
pub fn main() -> anyhow::Result<()> {
|
||||||
println!("cargo:rerun-if-changed=../extracted/");
|
println!("cargo:rerun-if-changed=../extracted/");
|
||||||
|
|
||||||
let generators = [
|
let generators = [
|
||||||
(block::build as fn() -> _, "block.rs"),
|
(block::build as fn() -> _, "block.rs"),
|
||||||
(item::build, "item.rs"),
|
|
||||||
(enchant::build, "enchant.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")?;
|
let out_dir = env::var_os("OUT_DIR").context("failed to get OUT_DIR env var")?;
|
||||||
|
|
43
valence_protocol/build/translation_key.rs
Normal file
43
valence_protocol/build/translation_key.rs
Normal 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)*
|
||||||
|
})
|
||||||
|
}
|
|
@ -112,6 +112,7 @@ mod item;
|
||||||
pub mod packets;
|
pub mod packets;
|
||||||
mod raw_bytes;
|
mod raw_bytes;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
pub mod translation_key;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod username;
|
pub mod username;
|
||||||
mod var_int;
|
mod var_int;
|
||||||
|
|
|
@ -885,23 +885,18 @@ mod tests {
|
||||||
assert_eq!(color_from_str("blue"), Some(Color::BLUE));
|
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]
|
#[test]
|
||||||
fn translate() {
|
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 serialized = serde_json::to_string(&txt).unwrap();
|
||||||
let deserialized: Text = serde_json::from_str(&serialized).unwrap();
|
let deserialized: Text = serde_json::from_str(&serialized).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
serialized,
|
serialized,
|
||||||
"{\"translate\":\"key\",\"with\":[{\"text\":\"arg1\"},{\"text\":\"arg2\"}]}"
|
"{\"translate\":\"chat.type.advancement.task\",\"with\":[{\"text\":\"arg1\"},{\"text\"\
|
||||||
|
:\"arg2\"}]}"
|
||||||
);
|
);
|
||||||
assert_eq!(txt, deserialized);
|
assert_eq!(txt, deserialized);
|
||||||
}
|
}
|
||||||
|
|
1
valence_protocol/src/translation_key.rs
Normal file
1
valence_protocol/src/translation_key.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/translation_key.rs"));
|
Loading…
Reference in a new issue