wiki multilang support
ci/woodpecker/push/deploy Pipeline was successful

This commit is contained in:
2026-08-28 20:52:49 +02:00
parent 653894a876
commit 9ffbc7a2ca
25 changed files with 351 additions and 52 deletions
@@ -5,23 +5,26 @@ class Api::RssController < ApiController
end
api :GET, "/api/rss/blog", "Blog RSS feed"
param :lang, String, required: false, desc: "Feed language (en, hu). Unknown values fall back to en"
returns code: 200, desc: "RSS XML feed of blog posts"
def blog
xml = Rss::BlogFeed.new.xml
xml = Rss::BlogFeed.new(lang: params[:lang]).xml
render xml: xml, content_type: "application/rss+xml"
end
api :GET, "/api/rss/releases", "Software releases RSS feed"
param :lang, String, required: false, desc: "Feed language (en, hu). Unknown values fall back to en"
returns code: 200, desc: "RSS XML feed of software releases"
def releases
xml = Rss::ReleasesFeed.new.xml
xml = Rss::ReleasesFeed.new(lang: params[:lang]).xml
render xml: xml, content_type: "application/rss+xml"
end
api :GET, "/api/rss/howtos", "HowTos RSS feed"
param :lang, String, required: false, desc: "Feed language (en, hu). Unknown values fall back to en"
returns code: 200, desc: "RSS XML feed of tech howtos"
def howtos
xml = Rss::HowtosFeed.new.xml
xml = Rss::HowtosFeed.new(lang: params[:lang]).xml
render xml: xml, content_type: "application/rss+xml"
end
end
@@ -7,8 +7,10 @@ class Api::WikiController < ApiController
param :tag, String, required: false, desc: "Filter by tag (blog, howto, engine)"
param :limit, :number, required: false, desc: "Limit number of results"
param :body, String, required: false, desc: "Include body content (1 = yes)"
param :lang, String, required: false, desc: "Content language (en, hu). Unknown values fall back to en"
returns code: 200, desc: "Wiki pages response" do
property :tag, String, desc: "Applied tag filter"
property :lang, String, desc: "Requested content language"
property :count, Integer, desc: "Number of pages returned"
property :pages, Array, desc: "Array of wiki pages" do
property :id, String, desc: "Page ID"
@@ -17,7 +19,7 @@ class Api::WikiController < ApiController
property :description, String, desc: "Short description"
property :createdAt, String, desc: "Created date (ISO 8601)"
property :updatedAt, String, desc: "Updated date (ISO 8601)"
property :locale, String, desc: "Locale code"
property :locale, String, desc: "Language the page body is actually written in (differs from lang when the page has no translation)"
property :route, String, desc: "URL slug"
property :tags, Array, of: String, desc: "Tags"
property :repo, String, desc: "Git repository URL (from page metadata, engines)"
@@ -31,7 +33,8 @@ class Api::WikiController < ApiController
render json: Wiki::Pages.new.fetch(
tag: params[:tag],
limit: params[:limit],
body: params[:body]
body: params[:body],
lang: params[:lang]
)
end
end
+3 -3
View File
@@ -2,12 +2,12 @@ module Rss
class BlogFeed < Feed
private
def title = "Teletype Games Blog"
def title = translate("rss.blog.title")
def link = "#{site_url}/blog"
def description = "Latest blog posts from Teletype Games"
def description = translate("rss.blog.description")
def items
Wiki::Pages.new.all(tag: "blog", limit: 30).map do |page|
Wiki::Pages.new.all(tag: "blog", limit: 30, lang: lang).map do |page|
{
title: page["title"],
link: "#{site_url}/blog/#{page['route']}",
+10 -2
View File
@@ -2,12 +2,16 @@ require "rss"
module Rss
class Feed
def initialize(lang: nil)
@lang = Locales.resolve(lang)
end
def xml
RSS::Maker.make("2.0") do |maker|
maker.channel.title = title
maker.channel.link = link
maker.channel.description = description
maker.channel.language = "hu"
maker.channel.language = lang
items.each do |item|
maker.items.new_item do |rss_item|
@@ -22,9 +26,13 @@ module Rss
private
attr_reader :lang
def site_url = Rails.configuration.x.site_url
def wiki_url = Rails.configuration.x.wiki_url
def wiki_url = "#{Rails.configuration.x.wiki_url}#{Wiki::Pages.path_prefix(lang)}"
def translate(key, **options) = I18n.t(key, locale: lang, **options)
def parse_time(value)
Time.parse(value.to_s)
+3 -3
View File
@@ -2,12 +2,12 @@ module Rss
class HowtosFeed < Feed
private
def title = "Teletype Games HowTos"
def title = translate("rss.howtos.title")
def link = "#{site_url}/howtos"
def description = "Latest tech howtos from Teletype Games"
def description = translate("rss.howtos.description")
def items
Wiki::Pages.new.all(tag: "howto", limit: 30).map do |page|
Wiki::Pages.new.all(tag: "howto", limit: 30, lang: lang).map do |page|
{
title: page["title"],
link: "#{wiki_url}/#{page['path']}",
+3 -3
View File
@@ -2,9 +2,9 @@ module Rss
class ReleasesFeed < Feed
private
def title = "Teletype Games Releases"
def title = translate("rss.releases.title")
def link = "#{site_url}/catalog"
def description = "Latest game releases from Teletype Games"
def description = translate("rss.releases.description")
def items
WarpEngine::Release.includes(:software).order(created_at: :desc).limit(50).filter_map do |release|
@@ -14,7 +14,7 @@ module Rss
{
title: "#{software.title} v#{release.version}",
link: "#{site_url}/catalog/#{software.name}",
description: "#{software.title} #{release.version} released #{software.desc}",
description: translate("rss.releases.item_description", title: software.title, version: release.version, desc: software.desc),
published_at: release.created_at.to_time
}
end
+14 -8
View File
@@ -3,12 +3,18 @@ require "json"
module Wiki
class Pages
def fetch(tag:, limit: nil, body: nil)
def self.path_prefix(language)
language == Locales::DEFAULT ? "" : "/#{language}"
end
def fetch(tag:, limit: nil, body: nil, lang: nil)
language = Locales.resolve(lang)
query = { tag: tag }
query[:limit] = limit if limit.present?
query[:body] = body if body.present?
uri = URI.parse("#{Rails.configuration.x.wiki_url}/custom/pages.json")
uri = URI.parse("#{Rails.configuration.x.wiki_url}#{self.class.path_prefix(language)}/custom/pages.json")
uri.query = URI.encode_www_form(query)
response = Net::HTTP.start(
@@ -17,21 +23,21 @@ module Wiki
open_timeout: 5, read_timeout: 10
) { |http| http.get(uri.request_uri) }
return empty(tag, "grav responded #{response.code}") unless response.is_a?(Net::HTTPSuccess)
return empty(tag, language, "grav responded #{response.code}") unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)
rescue StandardError => e
empty(tag, e.message)
empty(tag, language, e.message)
end
def all(tag:, limit: nil)
fetch(tag: tag, limit: limit).fetch("pages", [])
def all(tag:, limit: nil, lang: nil)
fetch(tag: tag, limit: limit, lang: lang).fetch("pages", [])
end
private
def empty(tag, error)
{ "tag" => tag, "count" => 0, "pages" => [], "error" => error }
def empty(tag, language, error)
{ "tag" => tag, "lang" => language, "count" => 0, "pages" => [], "error" => error }
end
end
end
+11
View File
@@ -5,3 +5,14 @@ en:
date:
formats:
long: "%Y-%m-%d"
rss:
blog:
title: "Teletype Games Blog"
description: "Latest blog posts from Teletype Games"
howtos:
title: "Teletype Games HowTos"
description: "Latest tech howtos from Teletype Games"
releases:
title: "Teletype Games Releases"
description: "Latest game releases from Teletype Games"
item_description: "%{title} %{version} released %{desc}"
+18
View File
@@ -0,0 +1,18 @@
hu:
time:
formats:
long: "%Y-%m-%d %H:%M"
date:
formats:
long: "%Y-%m-%d"
rss:
blog:
title: "Teletype Games blog"
description: "A Teletype Games legfrissebb blogbejegyzései"
howtos:
title: "Teletype Games útmutatók"
description: "A Teletype Games legfrissebb technikai útmutatói"
releases:
title: "Teletype Games kiadások"
description: "A Teletype Games legfrissebb játékkiadásai"
item_description: "Megjelent a %{title} %{version} %{desc}"
+9
View File
@@ -0,0 +1,9 @@
module Locales
SUPPORTED = %w[en hu].freeze
DEFAULT = "en"
def self.resolve(value)
normalized = value.to_s.strip.downcase
SUPPORTED.include?(normalized) ? normalized : DEFAULT
end
end
+20
View File
@@ -0,0 +1,20 @@
require "rails_helper"
RSpec.describe Locales do
describe ".resolve" do
it "accepts the supported languages" do
expect(Locales.resolve("en")).to eq("en")
expect(Locales.resolve("hu")).to eq("hu")
end
it "normalises case and whitespace" do
expect(Locales.resolve(" HU ")).to eq("hu")
end
it "falls back to the default for anything else" do
expect(Locales.resolve("de")).to eq(Locales::DEFAULT)
expect(Locales.resolve(nil)).to eq(Locales::DEFAULT)
expect(Locales.resolve("")).to eq(Locales::DEFAULT)
end
end
end
+31
View File
@@ -0,0 +1,31 @@
require "rails_helper"
RSpec.describe "Api::Wiki", type: :request do
let(:service) { instance_double(Wiki::Pages) }
before do
host! "teletypegames.org"
allow(Wiki::Pages).to receive(:new).and_return(service)
end
it "passes the requested language through to the wiki service" do
expect(service).to receive(:fetch)
.with(tag: "howto", limit: nil, body: nil, lang: "hu")
.and_return({ "tag" => "howto", "lang" => "hu", "count" => 0, "pages" => [] })
get "/api/wiki/pages", params: { tag: "howto", lang: "hu" }
expect(response).to have_http_status(:ok)
expect(response.parsed_body["lang"]).to eq("hu")
end
it "leaves the fallback to the service when no language is asked for" do
expect(service).to receive(:fetch)
.with(tag: "howto", limit: nil, body: nil, lang: nil)
.and_return({ "tag" => "howto", "lang" => "en", "count" => 0, "pages" => [] })
get "/api/wiki/pages", params: { tag: "howto" }
expect(response.parsed_body["lang"]).to eq("en")
end
end
+46
View File
@@ -0,0 +1,46 @@
require "rails_helper"
RSpec.describe Wiki::Pages do
let(:requested_paths) { [] }
before do
http = instance_double(Net::HTTP)
response = Net::HTTPOK.new("1.1", "200", "OK")
allow(response).to receive(:body).and_return({ tag: "howto", lang: "en", count: 0, pages: [] }.to_json)
allow(http).to receive(:get) do |path|
requested_paths << path
response
end
allow(Net::HTTP).to receive(:start) { |*_args, **_opts, &block| block.call(http) }
end
describe "#fetch" do
it "requests the default language without a path prefix" do
described_class.new.fetch(tag: "howto")
expect(requested_paths.first).to start_with("/custom/pages.json")
end
it "prefixes the path with the requested language" do
described_class.new.fetch(tag: "howto", lang: "hu")
expect(requested_paths.first).to start_with("/hu/custom/pages.json")
end
it "falls back to the default language for an unsupported one" do
described_class.new.fetch(tag: "howto", lang: "de")
expect(requested_paths.first).to start_with("/custom/pages.json")
end
it "reports the resolved language when grav is unreachable" do
allow(Net::HTTP).to receive(:start).and_raise(Errno::ECONNREFUSED)
result = described_class.new.fetch(tag: "howto", lang: "hu")
expect(result["lang"]).to eq("hu")
expect(result["pages"]).to eq([])
expect(result["error"]).to be_present
end
end
end