diff --git a/common/implementation/build.gradle.kts b/common/implementation/build.gradle.kts
index f9ed421d9..d4cde1973 100644
--- a/common/implementation/build.gradle.kts
+++ b/common/implementation/build.gradle.kts
@@ -21,6 +21,8 @@ dependencies {
"shadedApi"("commons-io:commons-io:2.6")
+ "shadedImplementation"("org.apache.commons:commons-text:1.9")
+
"compileOnly"("com.google.guava:guava:30.0-jre")
diff --git a/common/implementation/src/main/java/com/dfsek/terra/config/preprocessor/MetaStringPreprocessor.java b/common/implementation/src/main/java/com/dfsek/terra/config/preprocessor/MetaStringPreprocessor.java
new file mode 100644
index 000000000..d26721c36
--- /dev/null
+++ b/common/implementation/src/main/java/com/dfsek/terra/config/preprocessor/MetaStringPreprocessor.java
@@ -0,0 +1,38 @@
+package com.dfsek.terra.config.preprocessor;
+
+import com.dfsek.tectonic.config.Configuration;
+import com.dfsek.tectonic.exception.LoadException;
+import com.dfsek.tectonic.loading.ConfigLoader;
+import com.dfsek.tectonic.preprocessor.Result;
+import com.dfsek.terra.api.config.meta.Meta;
+import org.apache.commons.text.StringSubstitutor;
+import org.apache.commons.text.lookup.StringLookup;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.AnnotatedType;
+import java.util.Map;
+
+public class MetaStringPreprocessor extends MetaPreprocessor {
+ public MetaStringPreprocessor(Map configs) {
+ super(configs);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public @NotNull Result process(AnnotatedType t, T c, ConfigLoader loader, Meta annotation) {
+ if(String.class.equals(t.getType()) && c instanceof String) { // String is final so we use #equals
+ String candidate = (String) c;
+ StringSubstitutor substitutor = new StringSubstitutor(key -> {
+ Object meta = getMetaValue(key);
+ if(!(meta instanceof String)) {
+ throw new LoadException("MetaString template injection candidate must be string, is type " + meta.getClass().getCanonicalName());
+ }
+ return (String) meta;
+ });
+ return (Result) Result.overwrite(substitutor.replace(candidate));
+ }
+ return Result.noOp();
+ }
+
+
+}