diff --git a/src/blitter/32bpp_anim_sse4.hpp b/src/blitter/32bpp_anim_sse4.hpp
index 8e12b32236..aef968d988 100644
--- a/src/blitter/32bpp_anim_sse4.hpp
+++ b/src/blitter/32bpp_anim_sse4.hpp
@@ -40,9 +40,9 @@ public:
 	void Draw(const Blitter::BlitterParams *bp, SpriteCollKey sck);
 	void Draw(Blitter::BlitterParams *bp, BlitterMode mode, SpriteCollKey sck) override;
 
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) override
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) override
 	{
-		return Blitter_32bppSSE_Base::Encode(sprite_type, sprite, allocator);
+		return Blitter_32bppSSE_Base::Encode(sprite_type, sprite, has_rtl, allocator);
 	}
 	std::string_view GetName() override { return "32bpp-sse4-anim"; }
 	using Blitter_32bppSSE2_Anim::LookupColourInPalette;
diff --git a/src/blitter/32bpp_optimized.cpp b/src/blitter/32bpp_optimized.cpp
index 9aaf38d465..c69cb706fd 100644
--- a/src/blitter/32bpp_optimized.cpp
+++ b/src/blitter/32bpp_optimized.cpp
@@ -286,7 +286,7 @@ void Blitter_32bppOptimized::Draw(Blitter::BlitterParams *bp, BlitterMode mode,
 }
 
 template <bool Tpal_to_rgb>
-Sprite *Blitter_32bppOptimized::EncodeInternal(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)
+Sprite *Blitter_32bppOptimized::EncodeInternal(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator)
 {
 	/* streams of pixels (a, r, g, b channels)
 	 *
@@ -316,7 +316,7 @@ Sprite *Blitter_32bppOptimized::EncodeInternal(SpriteType sprite_type, const Spr
 		if (zoom_max == zoom_min) zoom_max = ZoomLevel::Max;
 	}
 
-	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max)) {
+	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max, has_rtl)) {
 		const SpriteLoader::Sprite *src_orig = &sprite[sck];
 
 		uint size = src_orig->height * src_orig->width;
@@ -411,26 +411,33 @@ Sprite *Blitter_32bppOptimized::EncodeInternal(SpriteType sprite_type, const Spr
 	}
 
 	uint len = 0; // total length of data
-	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max)) {
+	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max, has_rtl)) {
 		len += lengths[0][sck] + lengths[1][sck];
 	}
 
 	Sprite *dest_sprite = allocator.Allocate<Sprite>(sizeof(*dest_sprite) + sizeof(SpriteData) + len);
 
-	const auto &root_sprite = sprite.Root();
+	const auto &root_sprite = sprite.Root(false);
 	dest_sprite->height = root_sprite.height;
 	dest_sprite->width = root_sprite.width;
 	dest_sprite->x_offs = root_sprite.x_offs;
 	dest_sprite->y_offs = root_sprite.y_offs;
+	dest_sprite->has_rtl = has_rtl;
 
 	SpriteData *dst = (SpriteData *)dest_sprite->data;
 
 	uint32_t offset = 0;
-	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max)) {
+	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max, has_rtl)) {
 		dst->offset[0][sck] = offset;
 		offset += lengths[0][sck];
 		dst->offset[1][sck] = offset;
 		offset += lengths[1][sck];
+		if (!has_rtl) {
+			/* Duplicate the sprite for RTL */
+			SpriteCollKey rtl{sck.zoom, true};
+			dst->offset[0][rtl] = dst->offset[0][sck];
+			dst->offset[1][rtl] = dst->offset[1][sck];
+		}
 
 		std::copy_n(reinterpret_cast<uint8_t *>(dst_px_orig[sck].get()), lengths[0][sck], dst->data + dst->offset[0][sck]);
 		std::copy_n(reinterpret_cast<uint8_t *>(dst_n_orig[sck].get()), lengths[1][sck], dst->data + dst->offset[1][sck]);
@@ -439,10 +446,10 @@ Sprite *Blitter_32bppOptimized::EncodeInternal(SpriteType sprite_type, const Spr
 	return dest_sprite;
 }
 
-template Sprite *Blitter_32bppOptimized::EncodeInternal<true>(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator);
-template Sprite *Blitter_32bppOptimized::EncodeInternal<false>(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator);
+template Sprite *Blitter_32bppOptimized::EncodeInternal<true>(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator);
+template Sprite *Blitter_32bppOptimized::EncodeInternal<false>(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator);
 
-Sprite *Blitter_32bppOptimized::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)
+Sprite *Blitter_32bppOptimized::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator)
 {
-	return this->EncodeInternal<true>(sprite_type, sprite, allocator);
+	return this->EncodeInternal<true>(sprite_type, sprite, has_rtl, allocator);
 }
diff --git a/src/blitter/32bpp_optimized.hpp b/src/blitter/32bpp_optimized.hpp
index 1772502c4c..9137e0a37d 100644
--- a/src/blitter/32bpp_optimized.hpp
+++ b/src/blitter/32bpp_optimized.hpp
@@ -22,7 +22,7 @@ public:
 	};
 
 	void Draw(Blitter::BlitterParams *bp, BlitterMode mode, SpriteCollKey sck) override;
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) override;
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) override;
 
 	std::string_view GetName() override { return "32bpp-optimized"; }
 
@@ -30,7 +30,7 @@ public:
 
 protected:
 	template <bool Tpal_to_rgb> void Draw(Blitter::BlitterParams *bp, BlitterMode mode, SpriteCollKey sck);
-	template <bool Tpal_to_rgb> Sprite *EncodeInternal(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator);
+	template <bool Tpal_to_rgb> Sprite *EncodeInternal(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator);
 };
 
 /** Factory for the optimised 32 bpp blitter (without palette animation). */
diff --git a/src/blitter/32bpp_simple.cpp b/src/blitter/32bpp_simple.cpp
index 4d5053c444..97341600a1 100644
--- a/src/blitter/32bpp_simple.cpp
+++ b/src/blitter/32bpp_simple.cpp
@@ -115,9 +115,9 @@ void Blitter_32bppSimple::DrawColourMappingRect(void *dst, int width, int height
 	Debug(misc, 0, "32bpp blitter doesn't know how to draw this colour table ('{}')", pal);
 }
 
-Sprite *Blitter_32bppSimple::Encode(SpriteType, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)
+Sprite *Blitter_32bppSimple::Encode(SpriteType, const SpriteLoader::SpriteCollection &sprite, bool, SpriteAllocator &allocator)
 {
-	const auto &root_sprite = sprite.Root();
+	const auto &root_sprite = sprite.Root(false);
 	Blitter_32bppSimple::Pixel *dst;
 	Sprite *dest_sprite = allocator.Allocate<Sprite>(sizeof(*dest_sprite) + static_cast<size_t>(root_sprite.height) * static_cast<size_t>(root_sprite.width) * sizeof(*dst));
 
@@ -125,6 +125,7 @@ Sprite *Blitter_32bppSimple::Encode(SpriteType, const SpriteLoader::SpriteCollec
 	dest_sprite->width = root_sprite.width;
 	dest_sprite->x_offs = root_sprite.x_offs;
 	dest_sprite->y_offs = root_sprite.y_offs;
+	dest_sprite->has_rtl = false;
 
 	dst = reinterpret_cast<Blitter_32bppSimple::Pixel *>(dest_sprite->data);
 	SpriteLoader::CommonPixel *src = reinterpret_cast<SpriteLoader::CommonPixel *>(root_sprite.data);
diff --git a/src/blitter/32bpp_simple.hpp b/src/blitter/32bpp_simple.hpp
index 1c8eb1baf0..3d049dc858 100644
--- a/src/blitter/32bpp_simple.hpp
+++ b/src/blitter/32bpp_simple.hpp
@@ -26,7 +26,7 @@ class Blitter_32bppSimple : public Blitter_32bppBase {
 public:
 	void Draw(Blitter::BlitterParams *bp, BlitterMode mode, SpriteCollKey sck) override;
 	void DrawColourMappingRect(void *dst, int width, int height, PaletteID pal) override;
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) override;
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) override;
 
 	std::string_view GetName() override { return "32bpp-simple"; }
 };
diff --git a/src/blitter/32bpp_sse2.cpp b/src/blitter/32bpp_sse2.cpp
index e22ee0254f..26fd962b6c 100644
--- a/src/blitter/32bpp_sse2.cpp
+++ b/src/blitter/32bpp_sse2.cpp
@@ -20,7 +20,7 @@
 /** Instantiation of the SSE2 32bpp blitter factory. */
 static FBlitter_32bppSSE2 iFBlitter_32bppSSE2;
 
-Sprite *Blitter_32bppSSE_Base::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)
+Sprite *Blitter_32bppSSE_Base::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator)
 {
 	/* First uint32_t of a line = the number of transparent pixels from the left.
 	 * Second uint32_t of a line = the number of transparent pixels from the right.
@@ -37,7 +37,7 @@ Sprite *Blitter_32bppSSE_Base::Encode(SpriteType sprite_type, const SpriteLoader
 	/* Calculate sizes and allocate. */
 	SpriteData sd{};
 	uint all_sprites_size = 0;
-	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max)) {
+	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max, has_rtl)) {
 		const SpriteLoader::Sprite *src_sprite = &sprite[sck];
 		auto &info = sd.infos[sck];
 		info.sprite_width = src_sprite->width;
@@ -47,23 +47,30 @@ Sprite *Blitter_32bppSSE_Base::Encode(SpriteType sprite_type, const SpriteLoader
 		const uint rgba_size = info.sprite_line_size * src_sprite->height;
 		info.mv_offset = all_sprites_size + rgba_size;
 
+		if (!has_rtl) {
+			/* Duplicate the sprite for RTL */
+			SpriteCollKey rtl{sck.zoom, true};
+			sd.infos[rtl] = info;
+		}
+
 		const uint mv_size = sizeof(MapValue) * src_sprite->width * src_sprite->height;
 		all_sprites_size += rgba_size + mv_size;
 	}
 
 	Sprite *dst_sprite = allocator.Allocate<Sprite>(sizeof(Sprite) + sizeof(SpriteData) + all_sprites_size);
-	const auto &root_sprite = sprite.Root();
+	const auto &root_sprite = sprite.Root(false);
 	dst_sprite->height = root_sprite.height;
 	dst_sprite->width = root_sprite.width;
 	dst_sprite->x_offs = root_sprite.x_offs;
 	dst_sprite->y_offs = root_sprite.y_offs;
+	dst_sprite->has_rtl = has_rtl;
 	std::copy_n(reinterpret_cast<std::byte *>(&sd), sizeof(SpriteData), dst_sprite->data);
 
 	/* Copy colours and determine flags. */
 	bool has_remap = false;
 	bool has_anim = false;
 	bool has_translucency = false;
-	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max)) {
+	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max, has_rtl)) {
 		const SpriteLoader::Sprite *src_sprite = &sprite[sck];
 		const SpriteLoader::CommonPixel *src = (const SpriteLoader::CommonPixel *) src_sprite->data;
 		const auto &info = sd.infos[sck];
diff --git a/src/blitter/32bpp_sse2.hpp b/src/blitter/32bpp_sse2.hpp
index 175519b11f..5464b4a51c 100644
--- a/src/blitter/32bpp_sse2.hpp
+++ b/src/blitter/32bpp_sse2.hpp
@@ -77,7 +77,7 @@ public:
 		uint8_t data[]; ///< Data, all zoomlevels.
 	};
 
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator);
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator);
 };
 
 /** The SSE2 32 bpp blitter (without palette animation). */
@@ -87,9 +87,9 @@ public:
 	template <BlitterMode mode, Blitter_32bppSSE_Base::ReadMode read_mode, Blitter_32bppSSE_Base::BlockType bt_last, bool translucent>
 	void Draw(const Blitter::BlitterParams *bp, SpriteCollKey sck);
 
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) override
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) override
 	{
-		return Blitter_32bppSSE_Base::Encode(sprite_type, sprite, allocator);
+		return Blitter_32bppSSE_Base::Encode(sprite_type, sprite, has_rtl, allocator);
 	}
 
 	std::string_view GetName() override { return "32bpp-sse2"; }
diff --git a/src/blitter/40bpp_anim.cpp b/src/blitter/40bpp_anim.cpp
index 83476310fe..e5fcb2f491 100644
--- a/src/blitter/40bpp_anim.cpp
+++ b/src/blitter/40bpp_anim.cpp
@@ -400,9 +400,9 @@ void Blitter_40bppAnim::DrawColourMappingRect(void *dst, int width, int height,
 	}
 }
 
-Sprite *Blitter_40bppAnim::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)
+Sprite *Blitter_40bppAnim::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator)
 {
-	return this->EncodeInternal<false>(sprite_type, sprite, allocator);
+	return this->EncodeInternal<false>(sprite_type, sprite, has_rtl, allocator);
 }
 
 
diff --git a/src/blitter/40bpp_anim.hpp b/src/blitter/40bpp_anim.hpp
index 5be055ba99..8196501f08 100644
--- a/src/blitter/40bpp_anim.hpp
+++ b/src/blitter/40bpp_anim.hpp
@@ -27,7 +27,7 @@ public:
 	void ScrollBuffer(void *video, int &left, int &top, int &width, int &height, int scroll_x, int scroll_y) override;
 	void Draw(Blitter::BlitterParams *bp, BlitterMode mode, SpriteCollKey sck) override;
 	void DrawColourMappingRect(void *dst, int width, int height, PaletteID pal) override;
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) override;
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) override;
 	size_t BufferSize(uint width, uint height) override;
 	Blitter::PaletteAnimation UsePaletteAnimation() override;
 	bool NeedsAnimationBuffer() override;
diff --git a/src/blitter/8bpp_optimized.cpp b/src/blitter/8bpp_optimized.cpp
index 25631e748d..caf628fcb2 100644
--- a/src/blitter/8bpp_optimized.cpp
+++ b/src/blitter/8bpp_optimized.cpp
@@ -119,7 +119,7 @@ void Blitter_8bppOptimized::Draw(Blitter::BlitterParams *bp, BlitterMode mode, S
 	}
 }
 
-Sprite *Blitter_8bppOptimized::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)
+Sprite *Blitter_8bppOptimized::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator)
 {
 	/* Make memory for all zoom-levels */
 	uint memory = sizeof(SpriteData);
@@ -136,7 +136,7 @@ Sprite *Blitter_8bppOptimized::Encode(SpriteType sprite_type, const SpriteLoader
 		if (zoom_max == zoom_min) zoom_max = ZoomLevel::Max;
 	}
 
-	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max)) {
+	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max, has_rtl)) {
 		memory += sprite[sck].width * sprite[sck].height;
 	}
 
@@ -151,11 +151,16 @@ Sprite *Blitter_8bppOptimized::Encode(SpriteType sprite_type, const SpriteLoader
 	uint8_t *dst = temp_dst->data;
 
 	/* Make the sprites per zoom-level */
-	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max)) {
+	for (auto sck : SpriteCollKeyRange(zoom_min, zoom_max, has_rtl)) {
 		const SpriteLoader::Sprite &src_orig = sprite[sck];
 		/* Store the index table */
 		uint offset = dst - temp_dst->data;
 		temp_dst->offset[sck] = offset;
+		if (!has_rtl) {
+			/* Duplicate the sprite for RTL */
+			SpriteCollKey rtl{sck.zoom, true};
+			temp_dst->offset[rtl] = offset;
+		}
 
 		/* cache values, because compiler can't cache it */
 		int scaled_height = src_orig.height;
@@ -220,11 +225,12 @@ Sprite *Blitter_8bppOptimized::Encode(SpriteType sprite_type, const SpriteLoader
 	/* Allocate the exact amount of memory we need */
 	Sprite *dest_sprite = allocator.Allocate<Sprite>(sizeof(*dest_sprite) + size);
 
-	const auto &root_sprite = sprite.Root();
+	const auto &root_sprite = sprite.Root(false);
 	dest_sprite->height = root_sprite.height;
 	dest_sprite->width = root_sprite.width;
 	dest_sprite->x_offs = root_sprite.x_offs;
 	dest_sprite->y_offs = root_sprite.y_offs;
+	dest_sprite->has_rtl = has_rtl;
 	std::copy_n(reinterpret_cast<std::byte *>(temp_dst), size, dest_sprite->data);
 
 	return dest_sprite;
diff --git a/src/blitter/8bpp_optimized.hpp b/src/blitter/8bpp_optimized.hpp
index f910627bbc..babea7649c 100644
--- a/src/blitter/8bpp_optimized.hpp
+++ b/src/blitter/8bpp_optimized.hpp
@@ -23,7 +23,7 @@ public:
 	};
 
 	void Draw(Blitter::BlitterParams *bp, BlitterMode mode, SpriteCollKey sck) override;
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) override;
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) override;
 
 	std::string_view GetName() override { return "8bpp-optimized"; }
 };
diff --git a/src/blitter/8bpp_simple.cpp b/src/blitter/8bpp_simple.cpp
index f30064b3a4..3f520824c0 100644
--- a/src/blitter/8bpp_simple.cpp
+++ b/src/blitter/8bpp_simple.cpp
@@ -61,9 +61,9 @@ void Blitter_8bppSimple::Draw(Blitter::BlitterParams *bp, BlitterMode mode, Spri
 	}
 }
 
-Sprite *Blitter_8bppSimple::Encode(SpriteType, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)
+Sprite *Blitter_8bppSimple::Encode(SpriteType, const SpriteLoader::SpriteCollection &sprite, bool, SpriteAllocator &allocator)
 {
-	const auto &root_sprite = sprite.Root();
+	const auto &root_sprite = sprite.Root(false);
 	Sprite *dest_sprite;
 	dest_sprite = allocator.Allocate<Sprite>(sizeof(*dest_sprite) + static_cast<size_t>(root_sprite.height) * static_cast<size_t>(root_sprite.width));
 
@@ -71,6 +71,7 @@ Sprite *Blitter_8bppSimple::Encode(SpriteType, const SpriteLoader::SpriteCollect
 	dest_sprite->width = root_sprite.width;
 	dest_sprite->x_offs = root_sprite.x_offs;
 	dest_sprite->y_offs = root_sprite.y_offs;
+	dest_sprite->has_rtl = false;
 
 	/* Copy over only the 'remap' channel, as that is what we care about in 8bpp */
 	uint8_t *dst = reinterpret_cast<uint8_t *>(dest_sprite->data);
diff --git a/src/blitter/8bpp_simple.hpp b/src/blitter/8bpp_simple.hpp
index f2cc6d1e19..35957b8c55 100644
--- a/src/blitter/8bpp_simple.hpp
+++ b/src/blitter/8bpp_simple.hpp
@@ -17,7 +17,7 @@
 class Blitter_8bppSimple final : public Blitter_8bppBase {
 public:
 	void Draw(Blitter::BlitterParams *bp, BlitterMode mode, SpriteCollKey sck) override;
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) override;
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) override;
 
 	std::string_view GetName() override { return "8bpp-simple"; }
 };
diff --git a/src/blitter/null.cpp b/src/blitter/null.cpp
index 41b8a719b1..ef9f1f78ab 100644
--- a/src/blitter/null.cpp
+++ b/src/blitter/null.cpp
@@ -15,16 +15,17 @@
 /** Instantiation of the null blitter factory. */
 static FBlitter_Null iFBlitter_Null;
 
-Sprite *Blitter_Null::Encode(SpriteType, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)
+Sprite *Blitter_Null::Encode(SpriteType, const SpriteLoader::SpriteCollection &sprite, bool, SpriteAllocator &allocator)
 {
 	Sprite *dest_sprite;
 	dest_sprite = allocator.Allocate<Sprite>(sizeof(*dest_sprite));
 
-	const auto &root_sprite = sprite.Root();
+	const auto &root_sprite = sprite.Root(false);
 	dest_sprite->height = root_sprite.height;
 	dest_sprite->width = root_sprite.width;
 	dest_sprite->x_offs = root_sprite.x_offs;
 	dest_sprite->y_offs = root_sprite.y_offs;
+	dest_sprite->has_rtl = false;
 
 	return dest_sprite;
 }
diff --git a/src/blitter/null.hpp b/src/blitter/null.hpp
index 9718fdd19a..7f0c8f3335 100644
--- a/src/blitter/null.hpp
+++ b/src/blitter/null.hpp
@@ -18,7 +18,7 @@ public:
 	uint8_t GetScreenDepth() override { return 0; }
 	void Draw(Blitter::BlitterParams *, BlitterMode, SpriteCollKey) override {};
 	void DrawColourMappingRect(void *, int, int, PaletteID) override {};
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) override;
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) override;
 	void *MoveTo(void *, int, int) override { return nullptr; };
 	void SetPixel(void *, int, int, uint8_t) override {};
 	void DrawRect(void *, int, int, uint8_t) override {};
diff --git a/src/fontcache/freetypefontcache.cpp b/src/fontcache/freetypefontcache.cpp
index 390b77c130..19d2e90020 100644
--- a/src/fontcache/freetypefontcache.cpp
+++ b/src/fontcache/freetypefontcache.cpp
@@ -242,8 +242,8 @@ const Sprite *FreeTypeFontCache::InternalGetGlyph(GlyphID key, bool aa)
 
 	/* FreeType has rendered the glyph, now we allocate a sprite and copy the image into it */
 	SpriteLoader::SpriteCollection spritecollection;
-	SpriteLoader::Sprite &sprite = spritecollection.Root();
-	sprite.AllocateData(SpriteCollKey::Root(), static_cast<size_t>(width) * height);
+	SpriteLoader::Sprite &sprite = spritecollection.Root(false);
+	sprite.AllocateData(SpriteCollKey::Root(false), static_cast<size_t>(width) * height);
 	sprite.colours = SpriteComponent::Palette;
 	if (aa) sprite.colours.Set(SpriteComponent::Alpha);
 	sprite.width = width;
@@ -273,7 +273,7 @@ const Sprite *FreeTypeFontCache::InternalGetGlyph(GlyphID key, bool aa)
 	}
 
 	UniquePtrSpriteAllocator allocator;
-	BlitterFactory::GetCurrentBlitter()->Encode(SpriteType::Font, spritecollection, allocator);
+	BlitterFactory::GetCurrentBlitter()->Encode(SpriteType::Font, spritecollection, false, allocator);
 
 	GlyphEntry new_glyph;
 	new_glyph.data = std::move(allocator.data);
diff --git a/src/gfx.cpp b/src/gfx.cpp
index 35676d75c6..b790def13f 100644
--- a/src/gfx.cpp
+++ b/src/gfx.cpp
@@ -1167,7 +1167,7 @@ static void GfxBlitter(const Sprite * const sprite, int x, int y, BlitterMode mo
 		}
 	}
 
-	BlitterFactory::GetCurrentBlitter()->Draw(&bp, mode, SpriteCollKey{zoom});
+	BlitterFactory::GetCurrentBlitter()->Draw(&bp, mode, SpriteCollKey{zoom, _current_text_dir == TD_RTL});
 }
 
 /**
diff --git a/src/os/macosx/font_osx.cpp b/src/os/macosx/font_osx.cpp
index 0b448971f4..4516cb17b0 100644
--- a/src/os/macosx/font_osx.cpp
+++ b/src/os/macosx/font_osx.cpp
@@ -229,8 +229,8 @@ const Sprite *CoreTextFontCache::InternalGetGlyph(GlyphID key, bool use_aa)
 	if (width > MAX_GLYPH_DIM || height > MAX_GLYPH_DIM) UserError("Font glyph is too large");
 
 	SpriteLoader::SpriteCollection spritecollection;
-	SpriteLoader::Sprite &sprite = spritecollection.Root();
-	sprite.AllocateData(SpriteCollKey::Root(), width * height);
+	SpriteLoader::Sprite &sprite = spritecollection.Root(false);
+	sprite.AllocateData(SpriteCollKey::Root(false), width * height);
 	sprite.colours = SpriteComponent::Palette;
 	if (use_aa) sprite.colours.Set(SpriteComponent::Alpha);
 	sprite.width = width;
@@ -278,7 +278,7 @@ const Sprite *CoreTextFontCache::InternalGetGlyph(GlyphID key, bool use_aa)
 	}
 
 	UniquePtrSpriteAllocator allocator;
-	BlitterFactory::GetCurrentBlitter()->Encode(SpriteType::Font, spritecollection, allocator);
+	BlitterFactory::GetCurrentBlitter()->Encode(SpriteType::Font, spritecollection, false, allocator);
 
 	GlyphEntry new_glyph;
 	new_glyph.data = std::move(allocator.data);
diff --git a/src/os/windows/font_win32.cpp b/src/os/windows/font_win32.cpp
index 75c27674ea..b7e14648c9 100644
--- a/src/os/windows/font_win32.cpp
+++ b/src/os/windows/font_win32.cpp
@@ -224,8 +224,8 @@ void Win32FontCache::ClearFontCache()
 
 	/* GDI has rendered the glyph, now we allocate a sprite and copy the image into it. */
 	SpriteLoader::SpriteCollection spritecollection;
-	SpriteLoader::Sprite &sprite = spritecollection.Root();
-	sprite.AllocateData(SpriteCollKey::Root(), width * height);
+	SpriteLoader::Sprite &sprite = spritecollection.Root(false);
+	sprite.AllocateData(SpriteCollKey::Root(false), width * height);
 	sprite.colours = SpriteComponent::Palette;
 	if (aa) sprite.colours.Set(SpriteComponent::Alpha);
 	sprite.width = width;
@@ -264,7 +264,7 @@ void Win32FontCache::ClearFontCache()
 	}
 
 	UniquePtrSpriteAllocator allocator;
-	BlitterFactory::GetCurrentBlitter()->Encode(SpriteType::Font, spritecollection, allocator);
+	BlitterFactory::GetCurrentBlitter()->Encode(SpriteType::Font, spritecollection, false, allocator);
 
 	GlyphEntry new_glyph;
 	new_glyph.data = std::move(allocator.data);
diff --git a/src/settings_gui.cpp b/src/settings_gui.cpp
index 94a073d904..0b773a0fbf 100644
--- a/src/settings_gui.cpp
+++ b/src/settings_gui.cpp
@@ -1420,6 +1420,7 @@ struct GameOptionsWindow : Window {
 				CheckForMissingGlyphs();
 				ClearAllCachedNames();
 				UpdateAllVirtCoords();
+				VideoDriver::GetInstance()->ClearSystemSprites(); // relevant if _current_text_dir changes
 				CheckBlitter();
 				ReInitAllWindows(false);
 				break;
diff --git a/src/spritecache.cpp b/src/spritecache.cpp
index 2b5c0b3e50..cc19037f2f 100644
--- a/src/spritecache.cpp
+++ b/src/spritecache.cpp
@@ -249,8 +249,8 @@ static bool ResizeSpriteIn(SpriteLoader::SpriteCollection &sprite, SpriteCollKey
 
 static void ResizeSpriteOut(SpriteLoader::SpriteCollection &sprite, SpriteCollKey sck)
 {
-	const auto &root_sprite = sprite.Root();
-	const auto &src_sprite = sprite[SpriteCollKey{sck.zoom - 1}];
+	const auto &root_sprite = sprite.Root(sck.rtl);
+	const auto &src_sprite = sprite[SpriteCollKey{sck.zoom - 1, sck.rtl}];
 	auto &dest_sprite = sprite[sck];
 
 	/* Algorithm based on 32bpp_Optimized::ResizeSprite() */
@@ -337,7 +337,7 @@ static bool PadSprites(SpriteLoader::SpriteCollection &sprite, SpriteCollKeys sp
 	/* Get minimum top left corner coordinates. */
 	int min_xoffs = INT32_MAX;
 	int min_yoffs = INT32_MAX;
-	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min, ZoomLevel::Max)) {
+	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min, ZoomLevel::Max, true)) {
 		if (sprite_avail.Test(sck)) {
 			min_xoffs = std::min(min_xoffs, ScaleByZoom(sprite[sck].x_offs, sck.zoom));
 			min_yoffs = std::min(min_yoffs, ScaleByZoom(sprite[sck].y_offs, sck.zoom));
@@ -347,7 +347,7 @@ static bool PadSprites(SpriteLoader::SpriteCollection &sprite, SpriteCollKeys sp
 	/* Get maximum dimensions taking necessary padding at the top left into account. */
 	int max_width  = INT32_MIN;
 	int max_height = INT32_MIN;
-	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min, ZoomLevel::Max)) {
+	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min, ZoomLevel::Max, true)) {
 		if (sprite_avail.Test(sck)) {
 			max_width = std::max(max_width, ScaleByZoom(sprite[sck].width + sprite[sck].x_offs - UnScaleByZoom(min_xoffs, sck.zoom), sck.zoom));
 			max_height = std::max(max_height, ScaleByZoom(sprite[sck].height + sprite[sck].y_offs - UnScaleByZoom(min_yoffs, sck.zoom), sck.zoom));
@@ -362,7 +362,7 @@ static bool PadSprites(SpriteLoader::SpriteCollection &sprite, SpriteCollKeys sp
 	}
 
 	/* Pad sprites where needed. */
-	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min, ZoomLevel::Max)) {
+	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min, ZoomLevel::Max, true)) {
 		if (sprite_avail.Test(sck)) {
 			auto &cur_sprite = sprite[sck];
 			/* Scaling the sprite dimensions in the blitter is done with rounding up,
@@ -383,28 +383,34 @@ static bool PadSprites(SpriteLoader::SpriteCollection &sprite, SpriteCollKeys sp
 
 static bool ResizeSprites(SpriteLoader::SpriteCollection &sprite, SpriteCollKeys sprite_avail, SpriteEncoder *encoder)
 {
+	assert(sprite_avail.AnyLtr());
+	bool has_rtl = sprite_avail.AnyRtl();
+
 	/* Create a fully zoomed image if it does not exist */
-	ZoomLevel first_avail;
-	for (ZoomLevel zoom = ZoomLevel::Min; zoom <= ZoomLevel::Max; ++zoom) {
-		SpriteCollKey src_sck{zoom};
-		if (!sprite_avail.Test(src_sck)) continue;
-		first_avail = zoom;
-		if (zoom != ZoomLevel::Min) {
-			auto root_sck = SpriteCollKey::Root();
-			if (!ResizeSpriteIn(sprite, src_sck, root_sck)) return false;
-			sprite_avail.Set(root_sck);
+	ZoomLevel first_avail[2];
+	for (bool rtl : {false, true}) {
+		if (rtl && !has_rtl) continue;
+		for (ZoomLevel zoom = ZoomLevel::Min; zoom <= ZoomLevel::Max; ++zoom) {
+			SpriteCollKey src_sck{zoom, rtl};
+			if (!sprite_avail.Test(src_sck)) continue;
+			first_avail[rtl ? 1 : 0] = zoom;
+			if (zoom != ZoomLevel::Min) {
+				auto root_sck = SpriteCollKey::Root(rtl);
+				if (!ResizeSpriteIn(sprite, src_sck, root_sck)) return false;
+				sprite_avail.Set(root_sck);
+			}
+			break;
 		}
-		break;
 	}
 
 	/* Pad sprites to make sizes match. */
 	if (!PadSprites(sprite, sprite_avail, encoder)) return false;
 
 	/* Create other missing zoom levels */
-	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min + 1, ZoomLevel::Max)) {
+	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min + 1, ZoomLevel::Max, has_rtl)) {
 		if (sprite_avail.Test(sck)) {
 			/* Check that size and offsets match the fully zoomed image. */
-			[[maybe_unused]] const auto &root_sprite = sprite.Root();
+			[[maybe_unused]] const auto &root_sprite = sprite.Root(sck.rtl);
 			[[maybe_unused]] const auto &dest_sprite = sprite[sck];
 			assert(dest_sprite.width == UnScaleByZoom(root_sprite.width, sck.zoom));
 			assert(dest_sprite.height == UnScaleByZoom(root_sprite.height, sck.zoom));
@@ -417,11 +423,14 @@ static bool ResizeSprites(SpriteLoader::SpriteCollection &sprite, SpriteCollKeys
 	}
 
 	/* Replace sprites with higher resolution than the desired maximum source resolution with scaled up sprites, if not already done. */
-	if (first_avail < _settings_client.gui.sprite_zoom_min) {
-		for (ZoomLevel zoom = std::min(ZoomLevel::Normal, _settings_client.gui.sprite_zoom_min); zoom > ZoomLevel::Min; --zoom) {
-			const SpriteCollKey src_sck{zoom};
-			const SpriteCollKey dest_sck{zoom - 1};
-			ResizeSpriteIn(sprite, src_sck, dest_sck);
+	for (bool rtl : {false, true}) {
+		if (rtl && !has_rtl) continue;
+		if (first_avail[rtl ? 1 : 0] < _settings_client.gui.sprite_zoom_min) {
+			for (ZoomLevel zoom = std::min(ZoomLevel::Normal, _settings_client.gui.sprite_zoom_min); zoom > ZoomLevel::Min; --zoom) {
+				const SpriteCollKey src_sck{zoom, rtl};
+				const SpriteCollKey dest_sck{zoom - 1, rtl};
+				ResizeSpriteIn(sprite, src_sck, dest_sck);
+			}
 		}
 	}
 
@@ -498,16 +507,16 @@ static void *ReadSprite(const SpriteCache *sc, SpriteID id, SpriteType sprite_ty
 		/* Try for 32bpp sprites first. */
 		sprite_avail = sprite_loader.LoadSprite(sprite, file, file_pos, sprite_type, true, sc->control_flags, avail_8bpp, avail_32bpp);
 	}
-	if (sprite_avail.None()) {
+	if (sprite_avail.NoLtr()) {
 		sprite_avail = sprite_loader.LoadSprite(sprite, file, file_pos, sprite_type, false, sc->control_flags, avail_8bpp, avail_32bpp);
-		if (sprite_type == SpriteType::Normal && avail_32bpp.Any() && !encoder->Is32BppSupported() && sprite_avail.None()) {
+		if (sprite_type == SpriteType::Normal && avail_32bpp.AnyLtr() && !encoder->Is32BppSupported() && sprite_avail.NoLtr()) {
 			/* No 8bpp available, try converting from 32bpp. */
 			SpriteLoaderMakeIndexed make_indexed(sprite_loader);
 			sprite_avail = make_indexed.LoadSprite(sprite, file, file_pos, sprite_type, true, sc->control_flags, sprite_avail, avail_32bpp);
 		}
 	}
 
-	if (sprite_avail.None()) {
+	if (sprite_avail.NoLtr()) {
 		if (sprite_type == SpriteType::MapGen) return nullptr;
 		if (id == SPR_IMG_QUERY) UserError("Okay... something went horribly wrong. I couldn't load the fallback sprite. What should I do?");
 		return (void*)GetRawSprite(SPR_IMG_QUERY, SpriteType::Normal, &allocator, encoder);
@@ -523,7 +532,7 @@ static void *ReadSprite(const SpriteCache *sc, SpriteID id, SpriteType sprite_ty
 		 * Ugly: yes. Other solution: no. Blame the original author or
 		 *  something ;) The image should really have been a data-stream
 		 *  (so type = 0xFF basically). */
-		const auto &root_sprite = sprite.Root();
+		const auto &root_sprite = sprite.Root(false);
 		uint num = root_sprite.width * root_sprite.height;
 
 		Sprite *s = allocator.Allocate<Sprite>(sizeof(*s) + num);
@@ -549,12 +558,12 @@ static void *ReadSprite(const SpriteCache *sc, SpriteID id, SpriteType sprite_ty
 
 	if (sprite_type == SpriteType::Font && _font_zoom != ZoomLevel::Min) {
 		/* Make ZoomLevel::Min be ZOOM_LVL_GUI */
-		SpriteCollKey min_sck{ZoomLevel::Min};
-		SpriteCollKey font_sck{_font_zoom};
+		SpriteCollKey min_sck{ZoomLevel::Min, false};
+		SpriteCollKey font_sck{_font_zoom, false};
 		sprite[min_sck] = sprite[font_sck];
 	}
 
-	return encoder->Encode(sprite_type, sprite, allocator);
+	return encoder->Encode(sprite_type, sprite, sprite_avail.AnyRtl(), allocator);
 }
 
 struct GrfSpriteOffset {
diff --git a/src/spritecache.h b/src/spritecache.h
index 09fee98a92..ff497e4b5e 100644
--- a/src/spritecache.h
+++ b/src/spritecache.h
@@ -19,6 +19,7 @@ struct Sprite {
 	uint16_t width;  ///< Width of the sprite.
 	int16_t x_offs;  ///< Number of pixels to shift the sprite to the right.
 	int16_t y_offs;  ///< Number of pixels to shift the sprite downwards.
+	bool has_rtl; ///< Whether the sprite is textdir aware.
 	std::byte data[]; ///< Sprite data.
 };
 
diff --git a/src/spriteloader/grf.cpp b/src/spriteloader/grf.cpp
index 230cfbecd5..8e97ef0b45 100644
--- a/src/spriteloader/grf.cpp
+++ b/src/spriteloader/grf.cpp
@@ -230,7 +230,7 @@ static SpriteCollKeys LoadSpriteV1(SpriteLoader::SpriteCollection &sprite, Sprit
 	/* Type 0xFF indicates either a colourmap or some other non-sprite info; we do not handle them here */
 	if (type == 0xFF) return {};
 
-	const SpriteCollKey sck{(sprite_type != SpriteType::MapGen) ? ZoomLevel::Normal : ZoomLevel::Min};
+	const SpriteCollKey sck{(sprite_type != SpriteType::MapGen) ? ZoomLevel::Normal : ZoomLevel::Min, false};
 	auto &dest_sprite = sprite[sck];
 
 	dest_sprite.height = file.ReadByte();
@@ -280,6 +280,7 @@ static SpriteCollKeys LoadSpriteV2(SpriteLoader::SpriteCollection &sprite, Sprit
 		SpriteComponents colour{type};
 		/* Mask out colour component information from type. */
 		type &= ~SpriteComponents::MASK;
+		bool rtl = type & 0x10;
 
 		uint8_t zoom = file.ReadByte();
 
@@ -288,7 +289,7 @@ static SpriteCollKeys LoadSpriteV2(SpriteLoader::SpriteCollection &sprite, Sprit
 
 		if (sprite_type != SpriteType::MapGen) {
 			if (zoom < lengthof(zoom_lvl_map)) {
-				const SpriteCollKey sck{zoom_lvl_map[zoom]};
+				const SpriteCollKey sck{zoom_lvl_map[zoom], rtl};
 				if (colour == SpriteComponent::Palette) avail_8bpp.Set(sck);
 				if (colour != SpriteComponent::Palette) avail_32bpp.Set(sck);
 
@@ -309,8 +310,10 @@ static SpriteCollKeys LoadSpriteV2(SpriteLoader::SpriteCollection &sprite, Sprit
 			is_wanted_zoom_lvl = (zoom == 0);
 		}
 
-		if (is_wanted_colour_depth && is_wanted_zoom_lvl) {
-			const SpriteCollKey sck{sprite_type != SpriteType::MapGen ? zoom_lvl_map[zoom] : ZoomLevel::Min};
+		bool is_wanted_textdir = !rtl || sprite_type == SpriteType::Normal;
+
+		if (is_wanted_colour_depth && is_wanted_zoom_lvl && is_wanted_textdir) {
+			const SpriteCollKey sck{sprite_type != SpriteType::MapGen ? zoom_lvl_map[zoom] : ZoomLevel::Min, rtl};
 
 			if (loaded_sprites.Test(sck)) {
 				/* We already have this zoom level, skip sprite. */
diff --git a/src/spriteloader/makeindexed.cpp b/src/spriteloader/makeindexed.cpp
index c022c93c1e..7e8c267fab 100644
--- a/src/spriteloader/makeindexed.cpp
+++ b/src/spriteloader/makeindexed.cpp
@@ -52,7 +52,7 @@ SpriteCollKeys SpriteLoaderMakeIndexed::LoadSprite(SpriteLoader::SpriteCollectio
 {
 	SpriteCollKeys avail = this->baseloader.LoadSprite(sprite, file, file_pos, sprite_type, true, control_flags, avail_8bpp, avail_32bpp);
 
-	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min, ZoomLevel::Max)) {
+	for (auto sck : SpriteCollKeyRange(ZoomLevel::Min, ZoomLevel::Max, true)) {
 		if (avail.Test(sck)) Convert32bppTo8bpp(sprite[sck]);
 	}
 
diff --git a/src/spriteloader/spriteloader.hpp b/src/spriteloader/spriteloader.hpp
index 901cc94f2a..a4a77f6f30 100644
--- a/src/spriteloader/spriteloader.hpp
+++ b/src/spriteloader/spriteloader.hpp
@@ -31,27 +31,30 @@ using SpriteComponents = EnumBitSet<SpriteComponent, uint8_t, SpriteComponent::E
  */
 class SpriteCollKey {
 public:
+	bool rtl;
 	ZoomLevel zoom;
 
-	inline constexpr explicit SpriteCollKey(ZoomLevel zoom) : zoom(zoom) {}
+	inline constexpr SpriteCollKey(ZoomLevel zoom, bool rtl) : rtl(rtl), zoom(zoom) {}
 
 	inline constexpr bool operator==(const SpriteCollKey &rhs) const = default;
 	inline constexpr std::strong_ordering operator<=>(const SpriteCollKey &rhs) const = default;
 
-	static SpriteCollKey Root() { return SpriteCollKey(ZoomLevel::Min); }
+	static SpriteCollKey Root(bool rtl) { return SpriteCollKey(ZoomLevel::Min, rtl); }
 };
 
 /**
  * Set of sprite collection keys.
  */
 class SpriteCollKeys {
-	ZoomLevels keys;
+	ZoomLevels ltr, rtl;
 public:
 	inline constexpr SpriteCollKeys() = default;
-	inline constexpr void Set(const SpriteCollKey &sck) { this->keys.Set(sck.zoom); }
-	inline constexpr bool Test(const SpriteCollKey &sck) const { return this->keys.Test(sck.zoom); }
-	inline constexpr bool Any() const { return this->keys.Any(); }
-	inline constexpr bool None() const { return this->keys.None(); }
+	inline constexpr void Set(const SpriteCollKey &sck) { (sck.rtl ? this->rtl : this->ltr).Set(sck.zoom); }
+	inline constexpr bool Test(const SpriteCollKey &sck) const { return (sck.rtl ? this->rtl : this->ltr).Test(sck.zoom); }
+	inline constexpr bool AnyLtr() const { return this->ltr.Any(); }
+	inline constexpr bool AnyRtl() const { return this->rtl.Any(); }
+	inline constexpr bool NoLtr() const { return this->ltr.None(); }
+	inline constexpr bool NoRtl() const { return this->rtl.None(); }
 };
 
 /**
@@ -59,13 +62,13 @@ public:
  */
 template <class T>
 class SpriteCollMap {
-	std::array<T, to_underlying(ZoomLevel::End)> data{};
+	std::array<T, to_underlying(ZoomLevel::End)> ltr{}, rtl{};
 public:
-	inline constexpr T &operator[](const SpriteCollKey &sck) { return this->data[to_underlying(sck.zoom)]; }
-	inline constexpr const T &operator[](const SpriteCollKey &sck) const { return this->data[to_underlying(sck.zoom)]; }
+	inline constexpr T &operator[](const SpriteCollKey &sck) { return (sck.rtl ? this->rtl : this->ltr)[to_underlying(sck.zoom)]; }
+	inline constexpr const T &operator[](const SpriteCollKey &sck) const { return (sck.rtl ? this->rtl : this->ltr)[to_underlying(sck.zoom)]; }
 
-	T &Root() { return this->data[to_underlying(ZoomLevel::Min)]; }
-	const T &Root() const { return this->data[to_underlying(ZoomLevel::Min)]; }
+	T &Root(bool rtl) { return (rtl ? this->rtl : this->ltr)[to_underlying(ZoomLevel::Min)]; }
+	const T &Root(bool rtl) const { return (rtl ? this->rtl : this->ltr)[to_underlying(ZoomLevel::Min)]; }
 };
 
 /**
@@ -74,6 +77,7 @@ public:
 class SpriteCollKeyRange {
 public:
 	class Iterator {
+		ZoomLevel zoom_min, zoom_max;
 		SpriteCollKey pos;
 	public:
 		using value_type = SpriteCollKey;
@@ -82,14 +86,19 @@ public:
 		using pointer = void;
 		using reference = void;
 
-		Iterator(SpriteCollKey pos) : pos(pos) {}
+		Iterator(ZoomLevel zoom_min, ZoomLevel zoom_max, SpriteCollKey pos) : zoom_min(zoom_min), zoom_max(zoom_max), pos(pos) {}
 		bool operator==(const Iterator &rhs) const { return this->pos == rhs.pos; }
 		std::strong_ordering operator<=>(const Iterator &rhs) const { return this->pos <=> rhs.pos; }
 		const SpriteCollKey &operator*() const { return this->pos; }
 
 		Iterator& operator++()
 		{
-			++this->pos.zoom;
+			if (!this->pos.rtl && this->pos.zoom == this->zoom_max) {
+				this->pos.zoom = this->zoom_min;
+				this->pos.rtl = true;
+			} else {
+				++this->pos.zoom;
+			}
 			return *this;
 		}
 
@@ -102,7 +111,12 @@ public:
 
 		Iterator& operator--()
 		{
-			--this->pos.zoom;
+			if (this->pos.zoom == this->zoom_min) {
+				this->pos.zoom = this->zoom_max;
+				this->pos.rtl = false;
+			} else {
+				--this->pos.zoom;
+			}
 			return *this;
 		}
 
@@ -114,12 +128,13 @@ public:
 		}
 	};
 
-	SpriteCollKeyRange(ZoomLevel zoom_min, ZoomLevel zoom_max) : zoom_min(zoom_min), zoom_max(zoom_max) {}
-	Iterator begin() const { return Iterator{SpriteCollKey{this->zoom_min}}; }
-	Iterator end() const { return ++Iterator{SpriteCollKey{this->zoom_max}}; }
+	SpriteCollKeyRange(ZoomLevel zoom_min, ZoomLevel zoom_max, bool has_rtl) : zoom_min(zoom_min), zoom_max(zoom_max), has_rtl(has_rtl) {}
+	Iterator begin() const { return Iterator{this->zoom_min, this->zoom_max, SpriteCollKey{this->zoom_min, false}}; }
+	Iterator end() const { return ++Iterator{this->zoom_min, this->zoom_max, SpriteCollKey{this->zoom_max, this->has_rtl}}; }
 
 private:
 	ZoomLevel zoom_min, zoom_max;
+	bool has_rtl;
 };
 
 /** Interface for the loader of our sprites. */
@@ -221,7 +236,7 @@ public:
 	/**
 	 * Convert a sprite from the loader to our own format.
 	 */
-	virtual Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) = 0;
+	virtual Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) = 0;
 
 	/**
 	 * Get the value which the height and width on a sprite have to be aligned by.
diff --git a/src/video/opengl.cpp b/src/video/opengl.cpp
index f55f7f6416..d4b4ff18f9 100644
--- a/src/video/opengl.cpp
+++ b/src/video/opengl.cpp
@@ -34,6 +34,7 @@
 #include "../debug.h"
 #include "../blitter/factory.hpp"
 #include "../zoom_func.h"
+#include "../strings_func.h"
 #include "../core/string_consumer.hpp"
 
 #include "../table/opengl_shader.h"
@@ -1258,11 +1259,11 @@ void OpenGLBackend::ReleaseAnimBuffer(const Rect &update_rect)
 	}
 }
 
-/* virtual */ Sprite *OpenGLBackend::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator)
+/* virtual */ Sprite *OpenGLBackend::Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator)
 {
 	/* This encoding is only called for mouse cursors. We don't need real sprites but OpenGLSprites to show as cursor. These need to be put in the LRU cache. */
 	OpenGLSpriteAllocator &gl_allocator = static_cast<OpenGLSpriteAllocator&>(allocator);
-	gl_allocator.lru.Insert(gl_allocator.sprite, std::make_unique<OpenGLSprite>(sprite_type, sprite));
+	gl_allocator.lru.Insert(gl_allocator.sprite, std::make_unique<OpenGLSprite>(sprite_type, sprite, has_rtl && _current_text_dir == TD_RTL));
 
 	return nullptr;
 }
@@ -1397,9 +1398,9 @@ void OpenGLBackend::RenderOglSprite(const OpenGLSprite *gl_sprite, PaletteID pal
  * Create an OpenGL sprite with a palette remap part.
  * @param sprite The sprite to create the OpenGL sprite for
  */
-OpenGLSprite::OpenGLSprite(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite)
+OpenGLSprite::OpenGLSprite(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool rtl)
 {
-	const auto &root_sprite = sprite.Root();
+	const auto &root_sprite = sprite.Root(rtl);
 	this->dim.width = root_sprite.width;
 	this->dim.height = root_sprite.height;
 	this->x_offs = root_sprite.x_offs;
@@ -1441,7 +1442,7 @@ OpenGLSprite::OpenGLSprite(SpriteType sprite_type, const SpriteLoader::SpriteCol
 
 	/* Upload texture data. */
 	for (ZoomLevel zoom = ZoomLevel::Min; zoom <= (sprite_type == SpriteType::Font ? ZoomLevel::Min : ZoomLevel::Max); ++zoom) {
-		const auto &src_sprite = sprite[SpriteCollKey{zoom}];
+		const auto &src_sprite = sprite[SpriteCollKey{zoom, rtl}];
 		this->Update(src_sprite.width, src_sprite.height, to_underlying(zoom), src_sprite.data);
 	}
 
diff --git a/src/video/opengl.h b/src/video/opengl.h
index 75515f7210..009c26b07f 100644
--- a/src/video/opengl.h
+++ b/src/video/opengl.h
@@ -108,7 +108,7 @@ public:
 
 	bool Is32BppSupported() override { return true; }
 	uint GetSpriteAlignment() override { return 1u << to_underlying(ZoomLevel::Max); }
-	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, SpriteAllocator &allocator) override;
+	Sprite *Encode(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool has_rtl, SpriteAllocator &allocator) override;
 };
 
 
@@ -139,7 +139,7 @@ private:
 	bool BindTextures() const;
 
 public:
-	OpenGLSprite(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite);
+	OpenGLSprite(SpriteType sprite_type, const SpriteLoader::SpriteCollection &sprite, bool rtl);
 
 	/* No support for moving/copying the textures is implemented. */
 	OpenGLSprite(const OpenGLSprite&) = delete;