This commit is contained in:
@@ -27,6 +27,34 @@ The API is split in two layers:
|
||||
Devise/ActiveAdmin authentication, theming and assets. It consumes WarpEngine
|
||||
as a path gem and mounts it at `/`.
|
||||
|
||||
## Languages
|
||||
|
||||
The site is bilingual (English and Hungarian) and so is the wiki behind it.
|
||||
`Locales` (`lib/locales.rb`) is the one list of supported codes; anything else
|
||||
falls back to `en`.
|
||||
|
||||
The wiki serves English unprefixed and Hungarian under `/hu`, so
|
||||
`Wiki::Pages` builds the language into the request path rather than into a query
|
||||
parameter:
|
||||
|
||||
```
|
||||
GET /api/wiki/pages?tag=howto&lang=hu -> https://wiki.teletypegames.org/hu/custom/pages.json?tag=howto
|
||||
GET /api/wiki/pages?tag=howto -> https://wiki.teletypegames.org/custom/pages.json?tag=howto
|
||||
```
|
||||
|
||||
Each page in the response carries a `locale` saying which language its body is
|
||||
**actually** written in. That differs from the requested `lang` for the wiki
|
||||
pages that exist only in Hungarian: they are served as-is rather than 404ing, and
|
||||
a client can label them.
|
||||
|
||||
The RSS feeds take the same `lang` parameter (`/api/rss/blog?lang=hu`), which
|
||||
sets the channel language and picks the wiki language and the feed's own strings
|
||||
from `config/locales/`.
|
||||
|
||||
The frontend passes the interface language on every wiki call and rebuilds its
|
||||
outbound wiki links with `wikiUrl()`, so switching EN/HU re-reads the catalog and
|
||||
points the links at the matching wiki pages.
|
||||
|
||||
## The store registry
|
||||
|
||||
`GET /api/stores` lists the stores a client can install from. The desktop
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']}",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']}",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import { i18n } from '../../i18n'
|
||||
import { WIKI_BASE, wikiUrl } from '../wiki.api'
|
||||
|
||||
afterEach(() => {
|
||||
i18n.global.locale.value = 'en'
|
||||
})
|
||||
|
||||
describe('wikiUrl', () => {
|
||||
it('leaves English unprefixed', () => {
|
||||
expect(wikiUrl('development/godot', 'en')).toBe(`${WIKI_BASE}/development/godot`)
|
||||
})
|
||||
|
||||
it('prefixes Hungarian with /hu', () => {
|
||||
expect(wikiUrl('development/godot', 'hu')).toBe(`${WIKI_BASE}/hu/development/godot`)
|
||||
})
|
||||
|
||||
it('accepts a route that already starts with a slash', () => {
|
||||
expect(wikiUrl('/development/godot', 'hu')).toBe(`${WIKI_BASE}/hu/development/godot`)
|
||||
})
|
||||
|
||||
it('returns the wiki root when no page is given', () => {
|
||||
expect(wikiUrl('', 'hu')).toBe(`${WIKI_BASE}/hu`)
|
||||
expect(wikiUrl()).toBe(WIKI_BASE)
|
||||
})
|
||||
|
||||
it('falls back to English for an unsupported language', () => {
|
||||
expect(wikiUrl('development/godot', 'de')).toBe(`${WIKI_BASE}/development/godot`)
|
||||
})
|
||||
|
||||
it('follows the active interface language when none is passed', () => {
|
||||
i18n.global.locale.value = 'hu'
|
||||
expect(wikiUrl('development/godot')).toBe(`${WIKI_BASE}/hu/development/godot`)
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,27 @@
|
||||
import type { WikiPage, WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface'
|
||||
|
||||
import { CONFIG } from '../lib/config'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
const WIKI_BASE = CONFIG.wikiBase
|
||||
|
||||
const WIKI_LOCALES = ['en', 'hu'] as const
|
||||
const DEFAULT_WIKI_LOCALE = 'en'
|
||||
|
||||
function resolveLocale(locale?: string): string {
|
||||
const value = (locale ?? String(i18n.global.locale.value)).toLowerCase()
|
||||
return (WIKI_LOCALES as readonly string[]).includes(value) ? value : DEFAULT_WIKI_LOCALE
|
||||
}
|
||||
|
||||
// Grav serves English unprefixed and Hungarian under /hu, so a page link is the
|
||||
// base plus the prefix plus the language-neutral route.
|
||||
function wikiUrl(path = '', locale?: string): string {
|
||||
const lang = resolveLocale(locale)
|
||||
const prefix = lang === DEFAULT_WIKI_LOCALE ? '' : `/${lang}`
|
||||
const route = path ? `/${path.replace(/^\/+/, '')}` : ''
|
||||
return `${WIKI_BASE}${prefix}${route}`
|
||||
}
|
||||
|
||||
interface RawWikiPage {
|
||||
id: number
|
||||
path: string
|
||||
@@ -22,9 +40,9 @@ const HIGHLIGHTED_TAG = 'highlighted'
|
||||
|
||||
async function fetchPages(
|
||||
tag: string,
|
||||
opts: { body?: boolean; limit?: number } = {},
|
||||
opts: { body?: boolean; limit?: number; locale?: string } = {},
|
||||
): Promise<RawWikiPage[]> {
|
||||
const params = new URLSearchParams({ tag })
|
||||
const params = new URLSearchParams({ tag, lang: resolveLocale(opts.locale) })
|
||||
if (opts.body) params.set('body', '1')
|
||||
if (opts.limit) params.set('limit', String(opts.limit))
|
||||
|
||||
@@ -34,8 +52,8 @@ async function fetchPages(
|
||||
return json?.pages ?? []
|
||||
}
|
||||
|
||||
const listBlogPages = async (): Promise<WikiPageWithContent[]> => {
|
||||
const pages = await fetchPages('blog', { body: true })
|
||||
const listBlogPages = async (locale?: string): Promise<WikiPageWithContent[]> => {
|
||||
const pages = await fetchPages('blog', { body: true, locale })
|
||||
return pages.map((p): WikiPageWithContent => ({
|
||||
id: p.id,
|
||||
path: p.path,
|
||||
@@ -48,8 +66,8 @@ const listBlogPages = async (): Promise<WikiPageWithContent[]> => {
|
||||
}))
|
||||
}
|
||||
|
||||
const getBlogPage = async (slug: string): Promise<WikiPageContent | null> => {
|
||||
const pages = await fetchPages('blog', { body: true })
|
||||
const getBlogPage = async (slug: string, locale?: string): Promise<WikiPageContent | null> => {
|
||||
const pages = await fetchPages('blog', { body: true, locale })
|
||||
|
||||
const matched = pages.find((p) => {
|
||||
const pageSlug = p.path.startsWith('blog/') ? p.path.replace('blog/', '') : p.path
|
||||
@@ -70,8 +88,8 @@ const getBlogPage = async (slug: string): Promise<WikiPageContent | null> => {
|
||||
}
|
||||
}
|
||||
|
||||
const listEnginePages = async (): Promise<WikiPageWithContent[]> => {
|
||||
const pages = await fetchPages('engine', { body: true })
|
||||
const listEnginePages = async (locale?: string): Promise<WikiPageWithContent[]> => {
|
||||
const pages = await fetchPages('engine', { body: true, locale })
|
||||
return pages
|
||||
.filter((p) => (p.tags ?? []).includes(HIGHLIGHTED_TAG))
|
||||
.map((p): WikiPageWithContent => ({
|
||||
@@ -87,8 +105,8 @@ const listEnginePages = async (): Promise<WikiPageWithContent[]> => {
|
||||
}))
|
||||
}
|
||||
|
||||
const listHowtoPages = async (): Promise<WikiPage[]> => {
|
||||
const pages = await fetchPages('howto', { limit: 30 })
|
||||
const listHowtoPages = async (locale?: string): Promise<WikiPage[]> => {
|
||||
const pages = await fetchPages('howto', { limit: 30, locale })
|
||||
return pages.map((p): WikiPage => ({
|
||||
id: p.id,
|
||||
path: p.path,
|
||||
@@ -100,5 +118,5 @@ const listHowtoPages = async (): Promise<WikiPage[]> => {
|
||||
}))
|
||||
}
|
||||
|
||||
export { WIKI_BASE }
|
||||
export { WIKI_BASE, WIKI_LOCALES, DEFAULT_WIKI_LOCALE, wikiUrl }
|
||||
export default { listBlogPages, getBlogPage, listHowtoPages, listEnginePages }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { watch } from 'vue'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
// Wiki content is fetched per language, so anything cached from the wiki has to
|
||||
// be dropped and re-read when the visitor switches EN/HU.
|
||||
export function useLocaleReload(reload: () => void | Promise<void>) {
|
||||
watch(
|
||||
() => i18n.global.locale.value,
|
||||
() => {
|
||||
void reload()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
</nav>
|
||||
|
||||
<div class="footer-meta">
|
||||
<a :href="WIKI_BASE" target="_blank" rel="noopener noreferrer" class="footer-link">{{ t('footer.wiki') }}</a>
|
||||
<a :href="wikiUrl()" target="_blank" rel="noopener noreferrer" class="footer-link">{{ t('footer.wiki') }}</a>
|
||||
<a :href="GIT_BASE" target="_blank" rel="noopener noreferrer" class="footer-link">{{ t('footer.git') }}</a>
|
||||
<span class="footer-rss">
|
||||
<Rss v-bind="ICON" aria-hidden="true" />
|
||||
@@ -89,7 +89,7 @@ import { Menu, Moon, Rss, Sun, X } from 'lucide-vue-next'
|
||||
import { useUiStore } from '../stores/ui.store'
|
||||
import { getCookie } from '../lib/cookie'
|
||||
import { ICON, ICON_CONTROL } from '../components/icons'
|
||||
import { WIKI_BASE } from '../api/wiki.api'
|
||||
import { wikiUrl } from '../api/wiki.api'
|
||||
|
||||
const GIT_BASE = 'https://git.teletypegames.org'
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<a :href="exploreUrl(page)" target="_blank" rel="noopener" class="btn-accent">
|
||||
{{ t('engines.explore') }}
|
||||
</a>
|
||||
<a :href="`${WIKI_BASE}/${page.path}`" target="_blank" rel="noopener" class="btn-ghost">
|
||||
<a :href="wikiUrl(page.path)" target="_blank" rel="noopener" class="btn-ghost">
|
||||
{{ t('engines.openWiki') }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { WIKI_BASE } from '../../api/wiki.api'
|
||||
import { wikiUrl } from '../../api/wiki.api'
|
||||
import { useEnginesStore, getEngineDigest } from '../../stores/engines.store'
|
||||
import type { WikiPageWithContent } from '../../lib/interfaces/wiki.interface'
|
||||
import SkeletonCard from '../../components/SkeletonCard.vue'
|
||||
@@ -79,7 +79,7 @@ const cards = computed(() =>
|
||||
)
|
||||
|
||||
const exploreUrl = (page: WikiPageWithContent): string =>
|
||||
page.repo || `${WIKI_BASE}/${page.path}`
|
||||
page.repo || wikiUrl(page.path)
|
||||
|
||||
onMounted(() => store.fetch())
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<h1 class="hero-title">{{ t('howtos.title') }}</h1>
|
||||
<p class="hero-subtitle">{{ t('howtos.subtitle') }}</p>
|
||||
<div class="hero-actions">
|
||||
<a :href="WIKI_BASE" target="_blank" rel="noopener noreferrer" class="btn-accent">
|
||||
<a :href="wikiUrl()" target="_blank" rel="noopener noreferrer" class="btn-accent">
|
||||
{{ t('howtos.knowledgeBase') }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@
|
||||
<a
|
||||
v-for="page in recentPages"
|
||||
:key="page.id"
|
||||
:href="`${WIKI_BASE}/${page.path}`"
|
||||
:href="wikiUrl(page.path)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="howto-card"
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="!error && recentPages.length > 0" class="focus-row">
|
||||
<a :href="WIKI_BASE" target="_blank" rel="noopener noreferrer" class="link text-sm">
|
||||
<a :href="wikiUrl()" target="_blank" rel="noopener noreferrer" class="link text-sm">
|
||||
{{ t('howtos.browseWiki') }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@ import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import { WIKI_BASE } from '../../api/wiki.api'
|
||||
import { wikiUrl } from '../../api/wiki.api'
|
||||
import { useHowtosStore } from '../../stores/howtos.store'
|
||||
import SkeletonCard from '../../components/SkeletonCard.vue'
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<a :href="client.releasesUrl" target="_blank" rel="noopener noreferrer" class="btn-accent">
|
||||
{{ t('stores.clientDownload') }}
|
||||
</a>
|
||||
<a :href="client.wikiUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
<a :href="wikiUrl(client.wikiPath)" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
{{ t('stores.docs') }}
|
||||
</a>
|
||||
<a :href="client.repoUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
@@ -82,7 +82,7 @@
|
||||
</div>
|
||||
|
||||
<div class="st-links st-links-after">
|
||||
<a :href="current.wikiUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
<a :href="wikiUrl(current.wikiPath)" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
{{ t('stores.docs') }}
|
||||
</a>
|
||||
<a :href="current.repoUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
@@ -116,7 +116,7 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CONFIG } from '../../lib/config'
|
||||
import { wikiUrl } from '../../api/wiki.api'
|
||||
import CommandBlock from './CommandBlock.vue'
|
||||
import { BrandIcon } from '../../components/icons'
|
||||
import clientScreenshot from '../../assets/warpengine-client.webp'
|
||||
@@ -130,7 +130,7 @@ const FORGE = 'https://git.teletypegames.org/stores'
|
||||
const CLIENT = {
|
||||
repoUrl: `${FORGE}/warp-engine-client`,
|
||||
releasesUrl: `${FORGE}/warp-engine-client/releases`,
|
||||
wikiUrl: `${CONFIG.wikiBase}/stores/warp-engine-client`,
|
||||
wikiPath: 'stores/warp-engine-client',
|
||||
}
|
||||
|
||||
type DeviceId = 'batocera' | 'retroarch'
|
||||
@@ -150,7 +150,7 @@ const devices = [
|
||||
id: 'batocera' as DeviceId,
|
||||
projectUrl: 'https://batocera.org',
|
||||
repoUrl: `${FORGE}/ttg-batocera-store`,
|
||||
wikiUrl: `${CONFIG.wikiBase}/stores/ttg-batocera-store`,
|
||||
wikiPath: 'stores/ttg-batocera-store',
|
||||
platforms: [
|
||||
...CARTRIDGE_PLATFORMS,
|
||||
{ platform: 'ebitengine', label: 'Ebitengine', ext: '.zip', note: 'x86_64 · ARM64' },
|
||||
@@ -172,7 +172,7 @@ const devices = [
|
||||
id: 'retroarch' as DeviceId,
|
||||
projectUrl: 'https://www.retroarch.com',
|
||||
repoUrl: `${FORGE}/ttg-retroarch-store`,
|
||||
wikiUrl: `${CONFIG.wikiBase}/stores/ttg-retroarch-store`,
|
||||
wikiPath: 'stores/ttg-retroarch-store',
|
||||
platforms: CARTRIDGE_PLATFORMS,
|
||||
installCmd: `curl -fsSL ${FORGE}/ttg-retroarch-store/raw/branch/master/install.sh | sh`,
|
||||
afterInstallCmd: '',
|
||||
|
||||
@@ -74,3 +74,31 @@ describe('getEngineDigest', () => {
|
||||
expect(getEngineDigest('')).toEqual({ intro: '', highlightsTitle: '', highlights: [] })
|
||||
})
|
||||
})
|
||||
|
||||
const SAMPLE_MARKDOWN_HU = `
|
||||
> A példamotor egy **minta** keretrendszer, ami markdownból landing-kártyát csinál.
|
||||
|
||||
# Amit kapsz
|
||||
|
||||
- **Első funkció**: rövid magyarázattal.
|
||||
- Sima felsorolás, félkövér nyitás nélkül.
|
||||
|
||||
# Későbbi szakasz
|
||||
|
||||
- **Nem kerül be**: egy lista egy későbbi cím alatt.
|
||||
`
|
||||
|
||||
describe('getEngineDigest, Hungarian page', () => {
|
||||
it('recognises the Hungarian highlights heading', () => {
|
||||
const digest = getEngineDigest(SAMPLE_MARKDOWN_HU)
|
||||
expect(digest.highlightsTitle).toBe('Amit kapsz')
|
||||
expect(digest.highlights).toHaveLength(2)
|
||||
expect(digest.highlights[0]).toEqual({ title: 'Első funkció', text: 'rövid magyarázattal.' })
|
||||
})
|
||||
|
||||
it('still stops at the next heading', () => {
|
||||
expect(getEngineDigest(SAMPLE_MARKDOWN_HU).highlights.map((h) => h.title)).not.toContain(
|
||||
'Nem kerül be',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref } from 'vue'
|
||||
import wikiApi from '../api/wiki.api'
|
||||
import { isNew } from '../lib/softwareUtils'
|
||||
import { useLoadable } from '../composables/useLoadable'
|
||||
import { useLocaleReload } from '../composables/useLocaleReload'
|
||||
import type { WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface'
|
||||
|
||||
export const useBlogStore = defineStore('blog', () => {
|
||||
@@ -18,7 +19,10 @@ export const useBlogStore = defineStore('blog', () => {
|
||||
})
|
||||
}
|
||||
|
||||
const currentSlug = ref<string | null>(null)
|
||||
|
||||
async function fetchPage(slug: string) {
|
||||
currentSlug.value = slug
|
||||
currentPage.value = null
|
||||
await withPageCache(async () => {
|
||||
const result = await wikiApi.getBlogPage(slug)
|
||||
@@ -37,5 +41,11 @@ export const useBlogStore = defineStore('blog', () => {
|
||||
return content.replace(/[#*`_[\]()]/g, '').trim().slice(0, 300) + '...'
|
||||
}
|
||||
|
||||
useLocaleReload(async () => {
|
||||
invalidate()
|
||||
await fetch()
|
||||
if (currentSlug.value) await fetchPage(currentSlug.value)
|
||||
})
|
||||
|
||||
return { pages, loading, error, currentPage, pageLoading, pageError, fetch, fetchPage, isNew, getPermalink, getCleanPreview, invalidate }
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import wikiApi from '../api/wiki.api'
|
||||
import { useLoadable } from '../composables/useLoadable'
|
||||
import { useLocaleReload } from '../composables/useLocaleReload'
|
||||
import type { WikiPageWithContent } from '../lib/interfaces/wiki.interface'
|
||||
|
||||
export interface EngineHighlight {
|
||||
@@ -15,7 +16,7 @@ export interface EngineDigest {
|
||||
highlights: EngineHighlight[]
|
||||
}
|
||||
|
||||
const HIGHLIGHTS_HEADING = 'what you get'
|
||||
const HIGHLIGHTS_HEADINGS = ['what you get', 'amit kapsz']
|
||||
const MAX_HIGHLIGHTS = 6
|
||||
|
||||
function stripInline(md: string): string {
|
||||
@@ -39,7 +40,7 @@ export function getEngineDigest(content: string): EngineDigest {
|
||||
if (heading) {
|
||||
if (inHighlights) break
|
||||
const title = stripInline(heading[1])
|
||||
if (title.toLowerCase() !== HIGHLIGHTS_HEADING) break
|
||||
if (!HIGHLIGHTS_HEADINGS.includes(title.toLowerCase())) break
|
||||
inHighlights = true
|
||||
digest.highlightsTitle = title
|
||||
continue
|
||||
@@ -81,5 +82,10 @@ export const useEnginesStore = defineStore('engines', () => {
|
||||
return content.replace(/[#*`_[\]()>|-]/g, '').replace(/\s+/g, ' ').trim().slice(0, 260) + '...'
|
||||
}
|
||||
|
||||
useLocaleReload(async () => {
|
||||
invalidate()
|
||||
await fetch()
|
||||
})
|
||||
|
||||
return { pages, loading, error, fetch, getCleanPreview, getEngineDigest, invalidate }
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref } from 'vue'
|
||||
import wikiApi from '../api/wiki.api'
|
||||
import { isNew } from '../lib/softwareUtils'
|
||||
import { useLoadable } from '../composables/useLoadable'
|
||||
import { useLocaleReload } from '../composables/useLocaleReload'
|
||||
import type { WikiPage } from '../lib/interfaces/wiki.interface'
|
||||
|
||||
export const useHowtosStore = defineStore('howtos', () => {
|
||||
@@ -15,5 +16,10 @@ export const useHowtosStore = defineStore('howtos', () => {
|
||||
})
|
||||
}
|
||||
|
||||
useLocaleReload(async () => {
|
||||
invalidate()
|
||||
await fetch()
|
||||
})
|
||||
|
||||
return { pages, loading, error, fetch, isNew, invalidate }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user