diff --git a/src/game/game_text.hpp b/src/game/game_text.hpp
index 19a31049ec..14da7d9b2e 100644
--- a/src/game/game_text.hpp
+++ b/src/game/game_text.hpp
@@ -14,9 +14,6 @@
 
 #include "../core/smallvec_type.hpp"
 
-/** The tab we place our strings in. */
-static const uint GAME_TEXT_TAB = 18;
-
 const char *GetGameStringPtr(uint id);
 void RegisterGameTranslation(class Squirrel *engine);
 void ReconsiderGameScriptLanguage();
diff --git a/src/language.h b/src/language.h
index 8e09599c81..d33ba81892 100644
--- a/src/language.h
+++ b/src/language.h
@@ -22,9 +22,6 @@ static const uint8 CASE_GENDER_LEN = 16; ///< The (maximum) length of a case/gen
 static const uint8 MAX_NUM_GENDERS =  8; ///< Maximum number of supported genders.
 static const uint8 MAX_NUM_CASES   = 16; ///< Maximum number of supported cases.
 
-static const uint TAB_COUNT_BITS   = 5;                   ///< The number of bits used for the amount of tabs.
-static const uint TAB_COUNT        = 1 << TAB_COUNT_BITS; ///< The amount of tabs.
-
 /** Header of a language file. */
 struct LanguagePackHeader {
 	static const uint32 IDENT = 0x474E414C; ///< Identifier for OpenTTD language files, big endian for "LANG"
@@ -34,7 +31,7 @@ struct LanguagePackHeader {
 	char name[32];      ///< the international name of this language
 	char own_name[32];  ///< the localized name of this language
 	char isocode[16];   ///< the ISO code for the language (not country code)
-	uint16 offsets[TAB_COUNT]; ///< the offsets
+	uint16 offsets[TEXT_TAB_END]; ///< the offsets
 
 	/** Thousand separator used for anything not currencies */
 	char digit_group_separator[8];
diff --git a/src/newgrf_text.cpp b/src/newgrf_text.cpp
index 914d0ee1ae..279bd094c2 100644
--- a/src/newgrf_text.cpp
+++ b/src/newgrf_text.cpp
@@ -35,8 +35,6 @@
 
 #include "safeguards.h"
 
-#define GRFTAB  28
-
 /**
  * Explains the newgrf shift bit positioning.
  * the grf base will not be used in order to find the string, but rather for
@@ -695,7 +693,7 @@ StringID AddGRFString(uint32 grfid, uint16 stringid, byte langid_to_add, bool ne
 
 	grfmsg(3, "Added 0x%X: grfid %08X string 0x%X lang 0x%X string '%s'", id, grfid, stringid, newtext->langid, newtext->text);
 
-	return MakeStringID(GRFTAB, 0) + id; // Id reaches across multiple tabs
+	return MakeStringID(TEXT_TAB_NEWGRF1, 0) + id; // Id reaches across multiple tabs
 }
 
 /**
@@ -705,7 +703,7 @@ StringID GetGRFStringID(uint32 grfid, uint16 stringid)
 {
 	for (uint id = 0; id < _num_grf_texts; id++) {
 		if (_grf_text[id].grfid == grfid && _grf_text[id].stringid == stringid) {
-			return MakeStringID(GRFTAB, 0) + id; // Id reaches across multiple tabs
+			return MakeStringID(TEXT_TAB_NEWGRF1, 0) + id; // Id reaches across multiple tabs
 		}
 	}
 
diff --git a/src/saveload/company_sl.cpp b/src/saveload/company_sl.cpp
index ff02167d3e..2ad9759901 100644
--- a/src/saveload/company_sl.cpp
+++ b/src/saveload/company_sl.cpp
@@ -502,11 +502,11 @@ static void Check_PLYR()
 
 		/* We do not load old custom names */
 		if (IsSavegameVersionBefore(84)) {
-			if (GetStringTab(cprops->name_1) == 15) {
+			if (GetStringTab(cprops->name_1) == TEXT_TAB_OLD_CUSTOM) {
 				cprops->name_1 = STR_GAME_SAVELOAD_NOT_AVAILABLE;
 			}
 
-			if (GetStringTab(cprops->president_name_1) == 15) {
+			if (GetStringTab(cprops->president_name_1) == TEXT_TAB_OLD_CUSTOM) {
 				cprops->president_name_1 = STR_GAME_SAVELOAD_NOT_AVAILABLE;
 			}
 		}
diff --git a/src/saveload/strings_sl.cpp b/src/saveload/strings_sl.cpp
index c9fb91546a..aa2fdd3e60 100644
--- a/src/saveload/strings_sl.cpp
+++ b/src/saveload/strings_sl.cpp
@@ -61,7 +61,7 @@ char *_old_name_array = NULL;
 char *CopyFromOldName(StringID id)
 {
 	/* Is this name an (old) custom name? */
-	if (GetStringTab(id) != 15) return NULL;
+	if (GetStringTab(id) != TEXT_TAB_OLD_CUSTOM) return NULL;
 
 	if (IsSavegameVersionBefore(37)) {
 		/* Allow for expansion when converted to UTF-8. */
diff --git a/src/saveload/town_sl.cpp b/src/saveload/town_sl.cpp
index 8a9f879bba..3af5171b87 100644
--- a/src/saveload/town_sl.cpp
+++ b/src/saveload/town_sl.cpp
@@ -286,7 +286,7 @@ static void Load_TOWN()
 			SlObject(&t->received[i], _town_received_desc);
 		}
 
-		if (t->townnamegrfid == 0 && !IsInsideMM(t->townnametype, SPECSTR_TOWNNAME_START, SPECSTR_TOWNNAME_LAST + 1) && GetStringTab(t->townnametype) != 15) {
+		if (t->townnamegrfid == 0 && !IsInsideMM(t->townnametype, SPECSTR_TOWNNAME_START, SPECSTR_TOWNNAME_LAST + 1) && GetStringTab(t->townnametype) != TEXT_TAB_OLD_CUSTOM) {
 			SlErrorCorrupt("Invalid town name generator");
 		}
 
diff --git a/src/script/api/script_error.cpp b/src/script/api/script_error.cpp
index 87647cfe82..b692c74f44 100644
--- a/src/script/api/script_error.cpp
+++ b/src/script/api/script_error.cpp
@@ -34,15 +34,16 @@ ScriptError::ScriptErrorMapString ScriptError::error_map_string = ScriptError::S
 {
 	uint index = GetStringIndex(internal_string_id);
 	switch (GetStringTab(internal_string_id)) {
-		case 26: case 28: case 29: case 30: // NewGRF strings.
-			return ERR_NEWGRF_SUPPLIED_ERROR;
+		case TEXT_TAB_NEWGRF1:
+		case TEXT_TAB_NEWGRF2:
+		case TEXT_TAB_NEWGRF3:
+			return ERR_NEWGRF_SUPPLIED_ERROR; // NewGRF strings.
 
-		/* DO NOT SWAP case 14 and 4 because that will break StringToError due
-		 * to the index dependency that relies on FALL THROUGHs. */
-		case 14: if (index < 0xE4) break; // Player name
-		case 4:  if (index < 0xC0) break; // Town name
-		case 15: // Custom name
-		case 31: // Dynamic strings
+		case TEXT_TAB_SPECIAL:
+			if (index < 0xE4) break; // Player name
+			/* FALL THROUGH */
+		case TEXT_TAB_TOWN:
+			if (index < 0xC0) break; // Town name
 			/* These strings are 'random' and have no meaning.
 			 * They actually shouldn't even be returned as error messages. */
 			return ERR_UNKNOWN;
diff --git a/src/strings.cpp b/src/strings.cpp
index 704ed3c72e..0d115def95 100644
--- a/src/strings.cpp
+++ b/src/strings.cpp
@@ -187,20 +187,20 @@ struct LanguagePack : public LanguagePackHeader {
 
 static char **_langpack_offs;
 static LanguagePack *_langpack;
-static uint _langtab_num[TAB_COUNT];   ///< Offset into langpack offs
-static uint _langtab_start[TAB_COUNT]; ///< Offset into langpack offs
+static uint _langtab_num[TEXT_TAB_END];   ///< Offset into langpack offs
+static uint _langtab_start[TEXT_TAB_END]; ///< Offset into langpack offs
 static bool _scan_for_gender_data = false;  ///< Are we scanning for the gender of the current string? (instead of formatting it)
 
 
 const char *GetStringPtr(StringID string)
 {
 	switch (GetStringTab(string)) {
-		case GAME_TEXT_TAB: return GetGameStringPtr(GetStringIndex(string));
+		case TEXT_TAB_GAMESCRIPT: return GetGameStringPtr(GetStringIndex(string));
 		/* 0xD0xx and 0xD4xx IDs have been converted earlier. */
-		case 26: NOT_REACHED();
-		case 28: return GetGRFStringPtr(GetStringIndex(string));
-		case 29: return GetGRFStringPtr(GetStringIndex(string) + 0x0800);
-		case 30: return GetGRFStringPtr(GetStringIndex(string) + 0x1000);
+		case TEXT_TAB_OLD_NEWGRF: NOT_REACHED();
+		case TEXT_TAB_NEWGRF1: return GetGRFStringPtr(GetStringIndex(string));
+		case TEXT_TAB_NEWGRF2: return GetGRFStringPtr(GetStringIndex(string) + 0x0800);
+		case TEXT_TAB_NEWGRF3: return GetGRFStringPtr(GetStringIndex(string) + 0x1000);
 		default: return _langpack_offs[_langtab_start[GetStringTab(string)] + GetStringIndex(string)];
 	}
 }
@@ -220,42 +220,45 @@ char *GetStringWithArgs(char *buffr, StringID string, StringParameters *args, co
 	if (string == 0) return GetStringWithArgs(buffr, STR_UNDEFINED, args, last);
 
 	uint index = GetStringIndex(string);
-	uint tab = GetStringTab(string);
+	StringTab tab = GetStringTab(string);
 
 	switch (tab) {
-		case 4:
+		case TEXT_TAB_TOWN:
 			if (index >= 0xC0 && !game_script) {
 				return GetSpecialTownNameString(buffr, index - 0xC0, args->GetInt32(), last);
 			}
 			break;
 
-		case 14:
+		case TEXT_TAB_SPECIAL:
 			if (index >= 0xE4 && !game_script) {
 				return GetSpecialNameString(buffr, index - 0xE4, args, last);
 			}
 			break;
 
-		case 15:
+		case TEXT_TAB_OLD_CUSTOM:
 			/* Old table for custom names. This is no longer used */
 			if (!game_script) {
 				error("Incorrect conversion of custom name string.");
 			}
 			break;
 
-		case GAME_TEXT_TAB:
+		case TEXT_TAB_GAMESCRIPT:
 			return FormatString(buffr, GetGameStringPtr(index), args, last, case_index, true);
 
-		case 26:
+		case TEXT_TAB_OLD_NEWGRF:
 			NOT_REACHED();
 
-		case 28:
+		case TEXT_TAB_NEWGRF1:
 			return FormatString(buffr, GetGRFStringPtr(index), args, last, case_index);
 
-		case 29:
+		case TEXT_TAB_NEWGRF2:
 			return FormatString(buffr, GetGRFStringPtr(index + 0x0800), args, last, case_index);
 
-		case 30:
+		case TEXT_TAB_NEWGRF3:
 			return FormatString(buffr, GetGRFStringPtr(index + 0x1000), args, last, case_index);
+
+		default:
+			break;
 	}
 
 	if (index >= _langtab_num[tab]) {
@@ -886,7 +889,7 @@ static char *FormatString(char *buff, const char *str_arg, StringParameters *arg
 								buff = strecat(buff, "(invalid sub-StringID)", last);
 								break;
 							}
-							param = MakeStringID(GAME_TEXT_TAB, param);
+							param = MakeStringID(TEXT_TAB_GAMESCRIPT, param);
 						}
 
 						sub_args.SetParam(i++, param);
@@ -901,7 +904,7 @@ static char *FormatString(char *buff, const char *str_arg, StringParameters *arg
 				/* If we didn't error out, we can actually print the string. */
 				if (*str != '\0') {
 					str = p;
-					buff = GetStringWithArgs(buff, MakeStringID(GAME_TEXT_TAB, stringid), &sub_args, last, true);
+					buff = GetStringWithArgs(buff, MakeStringID(TEXT_TAB_GAMESCRIPT, stringid), &sub_args, last, true);
 				}
 
 				for (int i = 0; i < 20; i++) {
@@ -1017,7 +1020,7 @@ static char *FormatString(char *buff, const char *str_arg, StringParameters *arg
 
 			case SCC_STRING: {// {STRING}
 				StringID str = args->GetInt32(SCC_STRING);
-				if (game_script && GetStringTab(str) != GAME_TEXT_TAB) break;
+				if (game_script && GetStringTab(str) != TEXT_TAB_GAMESCRIPT) break;
 				/* WARNING. It's prohibited for the included string to consume any arguments.
 				 * For included strings that consume argument, you should use STRING1, STRING2 etc.
 				 * To debug stuff you can set argv to NULL and it will tell you */
@@ -1036,7 +1039,7 @@ static char *FormatString(char *buff, const char *str_arg, StringParameters *arg
 			case SCC_STRING7: { // {STRING1..7}
 				/* Strings that consume arguments */
 				StringID str = args->GetInt32(b);
-				if (game_script && GetStringTab(str) != GAME_TEXT_TAB) break;
+				if (game_script && GetStringTab(str) != TEXT_TAB_GAMESCRIPT) break;
 				uint size = b - SCC_STRING1 + 1;
 				if (game_script && size > args->GetDataLeft()) {
 					buff = strecat(buff, "(too many parameters)", last);
@@ -1736,13 +1739,13 @@ bool ReadLanguagePack(const LanguageMetadata *lang)
 	}
 
 #if TTD_ENDIAN == TTD_BIG_ENDIAN
-	for (uint i = 0; i < TAB_COUNT; i++) {
+	for (uint i = 0; i < TEXT_TAB_END; i++) {
 		lang_pack->offsets[i] = ReadLE16Aligned(&lang_pack->offsets[i]);
 	}
 #endif /* TTD_ENDIAN == TTD_BIG_ENDIAN */
 
 	uint count = 0;
-	for (uint i = 0; i < TAB_COUNT; i++) {
+	for (uint i = 0; i < TEXT_TAB_END; i++) {
 		uint16 num = lang_pack->offsets[i];
 		if (num > TAB_SIZE) {
 			free(lang_pack);
@@ -2051,12 +2054,12 @@ class LanguagePackGlyphSearcher : public MissingGlyphSearcher {
 
 	/* virtual */ const char *NextString()
 	{
-		if (this->i >= TAB_COUNT) return NULL;
+		if (this->i >= TEXT_TAB_END) return NULL;
 
 		const char *ret = _langpack_offs[_langtab_start[this->i] + this->j];
 
 		this->j++;
-		while (this->i < TAB_COUNT && this->j >= _langtab_num[this->i]) {
+		while (this->i < TEXT_TAB_END && this->j >= _langtab_num[this->i]) {
 			this->i++;
 			this->j = 0;
 		}
diff --git a/src/strings_func.h b/src/strings_func.h
index 3e3aa4840f..7ad20eb1f6 100644
--- a/src/strings_func.h
+++ b/src/strings_func.h
@@ -22,9 +22,9 @@
  * @param str String identifier
  * @return StringTab from \a str
  */
-static inline uint GetStringTab(StringID str)
+static inline StringTab GetStringTab(StringID str)
 {
-	return GB(str, TAB_SIZE_BITS, 5);
+	return (StringTab)GB(str, TAB_SIZE_BITS, 5);
 }
 
 /**
@@ -43,8 +43,9 @@ static inline uint GetStringIndex(StringID str)
  * @param index StringIndex
  * @return StringID composed from \a tab and \a index
  */
-static inline StringID MakeStringID(uint tab, uint index)
+static inline StringID MakeStringID(StringTab tab, uint index)
 {
+	assert(tab < TEXT_TAB_END);
 	assert(index < TAB_SIZE);
 	return tab << TAB_SIZE_BITS | index;
 }
diff --git a/src/strings_type.h b/src/strings_type.h
index 780221d0a8..098d3f94db 100644
--- a/src/strings_type.h
+++ b/src/strings_type.h
@@ -26,6 +26,24 @@ enum TextDirection {
 	TD_RTL, ///< Text is written right-to-left by default
 };
 
+/** StringTabs to group StringIDs */
+enum StringTab {
+	/* Tabs 0..1 for regular strings */
+	TEXT_TAB_TOWN             =  4,
+	TEXT_TAB_INDUSTRY         =  9,
+	TEXT_TAB_STATION          = 12,
+	TEXT_TAB_SPECIAL          = 14,
+	TEXT_TAB_OLD_CUSTOM       = 15,
+	TEXT_TAB_VEHICLE          = 16,
+	/* Tab 17 for regular strings */
+	TEXT_TAB_GAMESCRIPT       = 18,
+	TEXT_TAB_OLD_NEWGRF       = 26,
+	TEXT_TAB_NEWGRF1          = 28,
+	TEXT_TAB_NEWGRF2          = 29,
+	TEXT_TAB_NEWGRF3          = 30,
+	TEXT_TAB_END              = 32
+};
+
 /** Number of bits for the StringIndex within a StringTab */
 static const uint TAB_SIZE_BITS       = 11;
 /** Number of strings per StringTab */