diff --git a/bin/game/compat_14.nut b/bin/game/compat_14.nut
index 00efae0b65..757e2496f4 100644
--- a/bin/game/compat_14.nut
+++ b/bin/game/compat_14.nut
@@ -9,6 +9,9 @@
 
 GSBridge.GetBridgeID <- GSBridge.GetBridgeType;
 
+/* Emulate old GSText parameter padding behaviour */
+GSText.SCRIPT_TEXT_MAX_PARAMETERS <- 20;
+
 class GSCompat14 {
 	function Text(text)
 	{
diff --git a/src/game/game_instance.cpp b/src/game/game_instance.cpp
index 412e2b9204..1fc1df1fbd 100644
--- a/src/game/game_instance.cpp
+++ b/src/game/game_instance.cpp
@@ -50,9 +50,9 @@ void GameInstance::RegisterAPI()
 	/* Register all classes */
 	SQGS_RegisterAll(*this->engine);
 
-	RegisterGameTranslation(*this->engine);
-
 	if (!this->LoadCompatibilityScripts(GAME_DIR, GameInfo::ApiVersions)) this->Died();
+
+	RegisterGameTranslation(*this->engine);
 }
 
 int GameInstance::GetSetting(const std::string &name)
diff --git a/src/game/game_text.cpp b/src/game/game_text.cpp
index 92482a706b..84bc577531 100644
--- a/src/game/game_text.cpp
+++ b/src/game/game_text.cpp
@@ -370,6 +370,8 @@ void RegisterGameTranslation(Squirrel &engine)
 
 	sq_pop(vm, 2);
 
+	ScriptText::SetPadParameterCount(vm);
+
 	ReconsiderGameScriptLanguage();
 }
 
diff --git a/src/script/api/script_text.cpp b/src/script/api/script_text.cpp
index 4e1c02ea3e..87468f4226 100644
--- a/src/script/api/script_text.cpp
+++ b/src/script/api/script_text.cpp
@@ -57,7 +57,7 @@ ScriptText::ScriptText(HSQUIRRELVM vm)
 
 SQInteger ScriptText::_SetParam(int parameter, HSQUIRRELVM vm)
 {
-	if (parameter >= SCRIPT_TEXT_MAX_PARAMETERS) return SQ_ERROR;
+	if (static_cast<size_t>(parameter) >= std::size(this->param)) this->param.resize(parameter + 1);
 
 	switch (sq_gettype(vm, -1)) {
 		case OT_STRING: {
@@ -99,10 +99,13 @@ SQInteger ScriptText::_SetParam(int parameter, HSQUIRRELVM vm)
 			break;
 		}
 
+		case OT_NULL:
+			this->param[parameter] = {};
+			break;
+
 		default: return SQ_ERROR;
 	}
 
-	if (this->paramc <= parameter) this->paramc = parameter + 1;
 	return 0;
 }
 
@@ -113,7 +116,6 @@ SQInteger ScriptText::SetParam(HSQUIRRELVM vm)
 	SQInteger k;
 	sq_getinteger(vm, 2, &k);
 
-	if (k > SCRIPT_TEXT_MAX_PARAMETERS) return SQ_ERROR;
 	if (k < 1) return SQ_ERROR;
 	k--;
 
@@ -123,7 +125,7 @@ SQInteger ScriptText::SetParam(HSQUIRRELVM vm)
 SQInteger ScriptText::AddParam(HSQUIRRELVM vm)
 {
 	SQInteger res;
-	res = this->_SetParam(this->paramc, vm);
+	res = this->_SetParam(static_cast<int>(std::size(this->param)), vm);
 	if (res != 0) return res;
 
 	/* Push our own instance back on top of the stack */
@@ -140,7 +142,7 @@ SQInteger ScriptText::_set(HSQUIRRELVM vm)
 		sq_getstring(vm, 2, view);
 
 		std::string str = StrMakeValid(view);
-		if (!str.starts_with("param_") || str.size() > 8) return SQ_ERROR;
+		if (!str.starts_with("param_")) return SQ_ERROR;
 
 		auto key = ParseInteger<int32_t>(str.substr(6));
 		if (!key.has_value()) return SQ_ERROR;
@@ -153,13 +155,35 @@ SQInteger ScriptText::_set(HSQUIRRELVM vm)
 		return SQ_ERROR;
 	}
 
-	if (k > SCRIPT_TEXT_MAX_PARAMETERS) return SQ_ERROR;
 	if (k < 1) return SQ_ERROR;
 	k--;
 
 	return this->_SetParam(k, vm);
 }
 
+/**
+ * Set the number of padding parameters to use, for compatibility with old scripts.
+ * This is called during RegisterGameTranslation.
+ */
+void ScriptText::SetPadParameterCount(HSQUIRRELVM vm)
+{
+	ScriptText::pad_parameter_count = 0;
+
+	SQInteger top = sq_gettop(vm);
+	sq_pushroottable(vm);
+	sq_pushstring(vm, "GSText", -1);
+	if (!SQ_FAILED(sq_get(vm, -2))) {
+		sq_pushstring(vm, "SCRIPT_TEXT_MAX_PARAMETERS", -1);
+		if (!SQ_FAILED(sq_get(vm, -2))) {
+			SQInteger value;
+			if (!SQ_FAILED(sq_getinteger(vm, -1, &value))) {
+				ScriptText::pad_parameter_count = value;
+			}
+		}
+	}
+	sq_pop(vm, top);
+}
+
 EncodedString ScriptText::GetEncodedText()
 {
 	ScriptTextList seen_texts;
@@ -169,7 +193,6 @@ EncodedString ScriptText::GetEncodedText()
 	StringBuilder builder(result);
 	this->_FillParamList(params, seen_texts);
 	this->_GetEncodedText(builder, param_count, params, true);
-	if (param_count > SCRIPT_TEXT_MAX_PARAMETERS) throw Script_FatalError(fmt::format("{}: Too many parameters", GetGameStringName(this->string)));
 	return ::EncodedString{std::move(result)};
 }
 
@@ -178,21 +201,21 @@ void ScriptText::_FillParamList(ParamList &params, ScriptTextList &seen_texts)
 	if (std::ranges::find(seen_texts, this) != seen_texts.end()) throw Script_FatalError(fmt::format("{}: Circular reference detected", GetGameStringName(this->string)));
 	seen_texts.push_back(this);
 
-	for (int i = 0; i < this->paramc; i++) {
-		Param *p = &this->param[i];
-		params.emplace_back(this->string, i, p);
-		if (!std::holds_alternative<ScriptTextRef>(*p)) continue;
-		std::get<ScriptTextRef>(*p)->_FillParamList(params, seen_texts);
+	for (int idx = 0; Param &p : this->param) {
+		params.emplace_back(this->string, idx, &p);
+		++idx;
+		if (!std::holds_alternative<ScriptTextRef>(p)) continue;
+		std::get<ScriptTextRef>(p)->_FillParamList(params, seen_texts);
 	}
 
 	seen_texts.pop_back();
 
-	/* Fill with dummy parameters to match FormatString() behaviour. */
-	if (seen_texts.empty()) {
-		static Param dummy = 0;
-		int nb_extra = SCRIPT_TEXT_MAX_PARAMETERS - (int)params.size();
-		for (int i = 0; i < nb_extra; i++)
-			params.emplace_back(StringIndexInTab(-1), i, &dummy);
+	/* Fill with dummy parameters to match old FormatString() compatibility behaviour. */
+	if (seen_texts.empty() && ScriptText::pad_parameter_count > 0) {
+		static Param dummy = {};
+		for (int idx = static_cast<int>(std::size(this->param)); idx < ScriptText::pad_parameter_count; ++idx) {
+			params.emplace_back(StringIndexInTab(-1), idx, &dummy);
+		}
 	}
 }
 
@@ -204,6 +227,8 @@ void ScriptText::ParamCheck::Encode(StringBuilder &builder, std::string_view cmd
 	struct visitor {
 		StringBuilder &builder;
 
+		void operator()(const std::monostate &) { }
+
 		void operator()(std::string value)
 		{
 			this->builder.PutUtf8(SCC_ENCODED_STRING);
diff --git a/src/script/api/script_text.hpp b/src/script/api/script_text.hpp
index 50f4864358..62fd3ba587 100644
--- a/src/script/api/script_text.hpp
+++ b/src/script/api/script_text.hpp
@@ -75,8 +75,6 @@ private:
  */
 class ScriptText : public Text {
 public:
-	static const int SCRIPT_TEXT_MAX_PARAMETERS = 20; ///< The maximum amount of parameters you can give to one object.
-
 #ifndef DOXYGEN_API
 	/**
 	 * The constructor wrapper from Squirrel.
@@ -128,10 +126,15 @@ public:
 	 */
 	EncodedString GetEncodedText() override;
 
+	/**
+	 * @api -all
+	 */
+	static void SetPadParameterCount(HSQUIRRELVM vm);
+
 private:
 	using ScriptTextRef = ScriptObjectRef<ScriptText>;
 	using ScriptTextList = std::vector<ScriptText *>;
-	using Param = std::variant<SQInteger, std::string, ScriptTextRef>;
+	using Param = std::variant<std::monostate, SQInteger, std::string, ScriptTextRef>;
 
 	struct ParamCheck {
 		StringIndexInTab owner;
@@ -149,8 +152,9 @@ private:
 	using ParamSpan = std::span<ParamCheck>;
 
 	StringIndexInTab string;
-	std::array<Param, SCRIPT_TEXT_MAX_PARAMETERS> param = {};
-	int paramc = 0;
+	std::vector<Param> param{};
+
+	static inline int pad_parameter_count = 0; ///< Pad parameters for relaxed string validation.
 
 	/**
 	 * Internal function to recursively fill a list of parameters.