Localize all of the template text

This commit is contained in:
object-Object 2023-09-10 01:19:49 -04:00
parent 790521df05
commit 504ec5b48c
21 changed files with 269 additions and 88 deletions

View file

@ -16,6 +16,7 @@
"editor.rulers": [120],
},
"files.associations": {
"*.js.jinja": "javascript"
"*.js.jinja": "javascript",
"*.css.jinja": "css",
}
}

View file

@ -40,10 +40,8 @@ packages = [
]
[template.args]
title = "Hex Book"
mod_name = "Hex Casting"
author = "petrak@, Alwinfy"
description = "The Hex Book, all in one place."
icon_href = "logo.png"
show_landing_text = true

View file

@ -1,16 +1,75 @@
{
key: {
use: "Right Click",
sneak: "Left Shift",
jump: "Space",
hexdoc: {
hexcasting: {
title: "Hex Book",
description: "The Hex Book, all in one place.",
},
"item.minecraft": {
amethyst_shard: "Amethyst Shard",
budding_amethyst: "Budding Amethyst",
welcome: {
"1": "This is the online version of the %s documentation.",
"2": "Embedded images and patterns are included, but not crafting recipes or items. \
There's an in-game book for those.",
"3": "Additionally, this is built from the latest code on GitHub. \
It may describe $(bold)newer/$ features that you may not necessarily have, \
even on the latest Modrinth/CurseForge version!",
"4": "$(bold)Entries which are blurred are spoilers/$. \
Click to reveal them, but be aware that they may spoil endgame progression. \
Alternatively, click $(l:?nospoiler)here/$ to get a version with all spoilers showing.",
"old_version": "$(italic)The past is a foreign country; they do things differently there./$",
},
"block.hexcasting": {
slate: "Blank Slate",
toc: {
// use the span tag to make everything after it wrap with the toc button
// or put it at the very end to make the button wrap by itself
// either way, do NOT remove it entirely
title: "Table of {{ '<span class=\"nobr\">'|safe }}Contents",
toggle_all: "Toggle all",
},
pattern: {
show: "Click to show spell",
hide: "Click to hide spell",
multiplier: "${speedScale()}x",
paused: "Paused",
not_supported: "Your browser does not support visualizing patterns. Pattern code: %s",
},
pages: {
patchouli: {
crafting: {
description: "Depicted in the book: The crafting recipe for the",
separator: " and ",
},
},
hexcasting: {
crafting_multi: {
description: "Depicted in the book: Several crafting recipes, for the",
separator: ", ",
},
brainsweep: {
description: "Depicted in the book: A mind-flaying recipe producing the",
separator: "", // this value intentionally left blank
},
},
},
},
key: {
use: "Right Click",
sneak: "Left Shift",
jump: "Space",
},
"item.minecraft": {
amethyst_shard: "Amethyst Shard",
budding_amethyst: "Budding Amethyst",
},
"block.hexcasting": {
slate: "Blank Slate",
},
}

View file

@ -42,7 +42,7 @@
{# shows the names of all the recipe results in a list of recipes #}
{% macro recipe_block(recipes, result_attribute, description, separator) -%}
<blockquote class="crafting-info">
Depicted in the book: {{ description }} {{
{{ description }} {{
recipes
|map(attribute="result." ~ result_attribute)
|map("hexdoc_wrap", "code")

View file

@ -14,7 +14,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ props.url }}">{{ title }}</a>
<a class="navbar-brand" href="{{ props.url }}">{{ _("hexdoc."~props.modid~".title") }}</a>
</div>
<!-- content -->

View file

@ -4,11 +4,11 @@
<div id="table-of-contents"></div>
<nav class="toc-container">
<h2 class="page-header">
Table of <span class="nobr">Contents<a
{{ _("hexdoc.toc.title") }}<a
href="javascript:void(0)"
class="permalink toggle-link small"
data-target="toc-category"
title="Toggle all"
title="{{ _('hexdoc.toc.toggle_all') }}"
><i class="bi bi-list-nested"></i></a>{{ macros.permalink("table-of-contents", "toc-permalink") }}</span>
</h2>

View file

@ -1,26 +1,16 @@
<div class="container" style="margin-top: 3em;">
<blockquote>
<h1>This is the online version of the {{ mod_name }} documentation.</h1>
<p>
Embedded images and patterns are included, but not crafting recipes or items. There's an in-game book for those.
</p>
<h1>{{ _f("hexdoc.welcome.1")|format(mod_name) }}</h1>
<p>{{ _f("hexdoc.welcome.2") }}</p>
{% if is_bleeding_edge %}
<p>
Additionally, this is built from the latest code on GitHub.
It may describe <b>newer</b> features that you may not necessarily have, even on the latest CurseForge version!
</p>
<p>{{ _f("hexdoc.welcome.3") }}</p>
{% endif %}
<p>
<b>Entries which are blurred are spoilers</b>. Click to reveal them, but be aware that they may spoil endgame
progression. Alternatively, click <a href="?nospoiler">here</a> to get a version with all spoilers showing.
</p>
<p>{{ _f("hexdoc.welcome.4") }}</p>
{# conditionally revealed using js #}
<span id="old-version-notice" class="hidden">
<br />
<i>The past is a foreign country; they do things differently there.</i>
{{ _f("hexdoc.welcome.old_version") }}
</span>
</blockquote>
</div>

View file

@ -26,11 +26,11 @@ details[open] summary.collapse-spell {
}
details .collapse-spell::before {
content: "Click to show spell";
content: "{{ _('hexdoc.pattern.show') }}";
}
details[open] .collapse-spell::before {
content: "Click to hide spell";
content: "{{ _('hexdoc.pattern.hide') }}";
}
blockquote.crafting-info {

View file

@ -1,19 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }}</title>
<title>{{ _("hexdoc."~props.modid~".title") }}</title>
<link rel="icon" href="{{ icon_href }}">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="{{ description }}" />
<meta name="description" content="{{ _('hexdoc.'~props.modid~'.description') }}" />
<meta name="author" content="{{ author }}" />
<meta property="og:type" content="website" />
<meta property="og:title" content="{{ title }}" />
<meta property="og:title" content="{{ _('hexdoc.'~props.modid~'.title') }}" />
<meta property="og:image" content="{{ page_url }}/{{ icon_href }}" />
<meta property="og:url" content="{{ page_url }}" />
<meta property="og:description" content="{{ description }}" />
<meta property="og:description" content="{{ _('hexdoc.'~props.modid~'.description') }}" />
<meta property="og:site_name" content="{{ mod_name }}" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css"
@ -27,7 +27,7 @@
<style>
{% filter indent(6) %}
{%+ include_raw "main.css" %}
{%+ include "main.css.jinja" %}
{% endfilter %}
</style>

View file

@ -212,7 +212,9 @@ function initializeElem(canvas) {
})`;
context.font = `${pauseScale}px sans-serif`;
context.fillText(
speedScale() ? speedScale() + "x" : "Paused",
// these variables are filled by Jinja
// slightly scuffed, but it works for now
speedScale() ? `{{ _('hexdoc.pattern.multiplier') }}` : "{{ _('hexdoc.pattern.paused') }}",
0.2 * scale,
canvas.height - 0.2 * scale
);

View file

@ -1,7 +1,12 @@
{% extends "pages/patchouli/page.html.jinja" %}
{% include "common/macros.html.jinja" %}
{% include "common/macros.html.jinja" with context %}
{% block body %}
{{ macros.recipe_block([page.recipe], "name", "A mind-flaying recipe producing the", "") }}
{{ macros.recipe_block(
[page.recipe],
"name",
_("hexdoc.pages.hexcasting.brainsweep.description"),
_("hexdoc.pages.hexcasting.brainsweep.separator"),
) }}
{{ page.text|hexdoc_block }}
{% endblock body %}

View file

@ -1,7 +1,12 @@
{% extends "pages/patchouli/text.html.jinja" %}
{% include "common/macros.html.jinja" %}
{% include "common/macros.html.jinja" with context %}
{% block inner_body %}
{{ macros.recipe_block(page.recipes, "item", "Several crafting recipes, for the", ", ") }}
{{ macros.recipe_block(
page.recipes,
"item",
_("hexdoc.pages.hexcasting.crafting_multi.description"),
_("hexdoc.pages.hexcasting.crafting_multi.separator"),
) }}
{{ super() }}
{% endblock inner_body %}

View file

@ -11,7 +11,7 @@
data-string="{{ pattern.signature }}"
data-start="{{ pattern.startdir.name.lower() }}"
data-per-world="{{ pattern.is_per_world }}"
>Your browser does not support visualizing patterns. Pattern code: {{ pattern.signature }}</canvas>
>{{ _("hexdoc.pattern.not_supported")|format(pattern.signature) }}</canvas>
{% endfor %}
</details>
{{ super() }}

View file

@ -2,6 +2,11 @@
{% include "common/macros.html.jinja" %}
{% block inner_body %}
{{ macros.recipe_block(page.recipes, "item", "The crafting recipe for the", " and ") }}
{{ macros.recipe_block(
page.recipes,
"item",
_("hexdoc.pages.patchouli.crafting.description"),
_("hexdoc.pages.patchouli.crafting.separator"),
) }}
{{ super() }}
{% endblock inner_body %}

View file

@ -124,6 +124,9 @@ class Book(HexdocModel):
return self
context = cast_or_raise(info.context, BookContext)
# make the macros accessible when rendering the template
self.macros |= context.macros
self._link_bases: dict[tuple[ResourceLocation, str | None], str] = {}
# load categories

View file

@ -13,8 +13,7 @@ from pydantic.dataclasses import dataclass
from pydantic.functional_validators import ModelWrapValidatorHandler
from hexdoc.minecraft import LocalizedStr
from hexdoc.minecraft.i18n import I18nContext
from hexdoc.patchouli.text.html import HTMLElement, HTMLStream
from hexdoc.minecraft.i18n import I18n, I18nContext
from hexdoc.utils import DEFAULT_CONFIG, HexdocModel
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.utils.resource import ResourceLocation
@ -156,7 +155,12 @@ class Style(ABC, HexdocModel, frozen=True):
type: CommandStyleType | FunctionStyleType | SpecialStyleType
@staticmethod
def parse(style_str: str, context: FormattingContext) -> Style | _CloseTag | str:
def parse(
style_str: str,
book_id: ResourceLocation,
i18n: I18n,
is_0_black: bool,
) -> Style | _CloseTag | str:
# direct text replacements
if style_str in _REPLACEMENTS:
return _REPLACEMENTS[style_str]
@ -170,7 +174,7 @@ class Style(ABC, HexdocModel, frozen=True):
return CommandStyle(type=style_type)
# reset color, but only if 0 is considered reset instead of black
if not context.props.is_0_black and style_str == "0":
if style_str == "0" and not is_0_black:
return _CloseTag(type=SpecialStyleType.color)
# preset colors
@ -187,11 +191,11 @@ class Style(ABC, HexdocModel, frozen=True):
# keys
if name == "k":
return str(context.i18n.localize_key(value))
return str(i18n.localize_key(value))
# links
if name == SpecialStyleType.link.value:
return LinkStyle(value=_format_href(value, context.book_id))
return LinkStyle(value=_format_href(value, book_id))
# all the other functions
if style_type := FunctionStyleType.get(name):
@ -224,7 +228,8 @@ def is_external_link(value: str) -> bool:
def _format_href(value: str, book_id: ResourceLocation) -> str | BookLink:
if is_external_link(value):
# TODO: kinda hacky, BookLink should *probably* support query params
if value.startswith("?") or is_external_link(value):
return value
return BookLink.from_str(value, book_id)
@ -326,13 +331,21 @@ class FormatTree:
children: list[FormatTree | str] # this can't be Self, it breaks Pydantic
@classmethod
def format(cls, string: str, context: FormattingContext) -> Self:
def format(
cls,
string: str,
*,
book_id: ResourceLocation,
i18n: I18n,
macros: dict[str, str],
is_0_black: bool,
) -> Self:
# resolve macros
# this could use ahocorasick, but it works fine for now
old_string = None
while old_string != string:
old_string = string
for macro, replace in context.macros.items():
for macro, replace in macros.items():
string = string.replace(macro, replace)
# lex out parsed styles
@ -347,7 +360,7 @@ class FormatTree:
text_since_prev_style.append(leading_text)
last_end = match.end()
match Style.parse(match[1], context):
match Style.parse(match[1], book_id, i18n, is_0_black):
case str(replacement):
# str means "use this instead of the original value"
text_since_prev_style.append(replacement)
@ -404,11 +417,18 @@ class FormatTree:
):
if not info.context or isinstance(value, FormatTree):
return handler(value)
context = cast_or_raise(info.context, FormattingContext)
context = cast_or_raise(info.context, FormattingContext)
if isinstance(value, str):
value = context.i18n.localize(value)
return cls.format(value.value, context)
return cls.format(
value.value,
book_id=context.book_id,
i18n=context.i18n,
macros=context.macros,
is_0_black=context.props.is_0_black,
)
FormatTree._wrap_root

View file

@ -1,3 +1,6 @@
# pyright: reportUnknownMemberType=false, reportUnknownArgumentType=false
# pyright: reportUnknownLambdaType=false
import io
import json
import logging
@ -24,7 +27,12 @@ from hexdoc.patchouli import Book
from hexdoc.plugin import PluginManager
from hexdoc.utils import HexdocModel, ModResourceLoader, Properties
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.utils.jinja_extensions import IncludeRawExtension, hexdoc_block, hexdoc_wrap
from hexdoc.utils.jinja_extensions import (
IncludeRawExtension,
hexdoc_block,
hexdoc_localize,
hexdoc_wrap,
)
from hexdoc.utils.path import write_to_path
MARKER_NAME = ".sitemap-marker.json"
@ -151,7 +159,7 @@ def main(args: Args | None = None) -> None:
# load everything
with ModResourceLoader.clean_and_load_all(props, pm) as loader:
books = dict[str, Book]()
books = dict[str, tuple[Book, I18n]]()
if args.lang:
first_lang = args.lang
@ -177,12 +185,15 @@ def main(args: Args | None = None) -> None:
# load one book with exporting enabled
first_i18n = per_lang_i18n.pop(first_lang)
books[first_lang] = load_hex_book(book_data, pm, loader, first_i18n)
books[first_lang] = (
load_hex_book(book_data, pm, loader, first_i18n),
first_i18n,
)
# then load the rest with exporting disabled for efficiency
loader.export_dir = None
for lang, i18n in per_lang_i18n.items():
books[lang] = load_hex_book(book_data, pm, loader, i18n)
books[lang] = (load_hex_book(book_data, pm, loader, i18n), i18n)
if args.export_only:
return
@ -203,10 +214,10 @@ def main(args: Args | None = None) -> None:
IncludeRawExtension,
],
)
env.filters |= { # type: ignore
env.filters |= { # pyright: ignore[reportGeneralTypeIssues]
"hexdoc_block": hexdoc_block,
"hexdoc_wrap": hexdoc_wrap,
"hexdoc_localize": hexdoc_localize,
}
template = env.get_template(props.template.main)
@ -218,27 +229,53 @@ def main(args: Args | None = None) -> None:
shutil.rmtree(output_dir, ignore_errors=True)
if args.update_latest:
render_books(props, books, template, output_dir, "latest")
render_books(
props=props,
books=books,
template=template,
output_dir=output_dir,
allow_missing=args.allow_missing,
version="latest",
is_root=False,
)
if args.is_release:
render_books(props, books, template, output_dir, version)
render_books(
props=props,
books=books,
template=template,
output_dir=output_dir,
allow_missing=args.allow_missing,
version=version,
is_root=False,
)
# the default book should be the latest released version
if args.update_latest and args.is_release:
render_books(props, books, template, output_dir, version, is_root=True)
render_books(
props=props,
books=books,
template=template,
output_dir=output_dir,
allow_missing=args.allow_missing,
version=version,
is_root=True,
)
logger.info("Done.")
def render_books(
*,
props: Properties,
books: dict[str, Book],
books: dict[str, tuple[Book, I18n]],
template: Template,
output_dir: Path,
allow_missing: bool,
version: str,
is_root: bool = False,
is_root: bool,
):
for lang, book in books.items():
for lang, (book, i18n) in books.items():
# /index.html
# /lang/index.html
# /v/version/index.html
@ -254,17 +291,35 @@ def render_books(
page_url = "/".join([props.url, *path.parts])
logging.getLogger(__name__).info(f"Rendering {output_dir}")
docs = strip_empty_lines(
template.render(
**props.template.args,
book=book,
raw_docs = template.render(
**props.template.args,
book=book,
props=props,
page_url=page_url,
version=version,
lang=lang,
is_bleeding_edge=version == "latest",
# i18n helper
_=lambda key: hexdoc_localize(
key,
do_format=False,
props=props,
page_url=page_url,
version=version,
lang=lang,
is_bleeding_edge=version == "latest",
)
book=book,
i18n=i18n,
allow_missing=allow_missing,
),
# i18n helper, but with patchi formatting
_f=lambda key: hexdoc_localize(
key,
do_format=True,
props=props,
book=book,
i18n=i18n,
allow_missing=allow_missing,
),
)
docs = strip_empty_lines(raw_docs)
write_to_path(output_dir / "index.html", docs)
if props.template.static_dir:

View file

@ -6,11 +6,14 @@ from jinja2.parser import Parser
from jinja2.runtime import Context
from markupsafe import Markup
from hexdoc.minecraft import LocalizedStr
from hexdoc.patchouli import FormatTree
from hexdoc.minecraft import I18n, LocalizedStr
from hexdoc.patchouli import Book, FormatTree
from hexdoc.patchouli.book import Book
from hexdoc.patchouli.text import HTMLStream
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.patchouli.text.formatting import FormatTree
from . import Properties
from .deserialize import cast_or_raise
# https://stackoverflow.com/a/64392515
@ -30,15 +33,16 @@ class IncludeRawExtension(Extension):
@pass_context
def hexdoc_block(context: Context, value: Any) -> str:
def hexdoc_block(context: Context | dict[{"book": Book}], value: Any) -> str:
try:
return _hexdoc_block(context, value)
book = cast_or_raise(context["book"], Book)
return _hexdoc_block(book, value)
except Exception as e:
e.add_note(f"Value:\n {value}")
raise
def _hexdoc_block(context: Context, value: Any) -> str:
def _hexdoc_block(book: Book, value: Any) -> str:
match value:
case LocalizedStr() | str():
# use Markup to tell Jinja not to escape this string for us
@ -46,11 +50,10 @@ def _hexdoc_block(context: Context, value: Any) -> str:
return Markup("<br />".join(Markup.escape(line) for line in lines))
case FormatTree():
book = cast_or_raise(context["book"], Book)
with HTMLStream() as out:
with value.style.element(out, book.link_bases):
for child in value.children:
out.write(_hexdoc_block(context, child))
out.write(_hexdoc_block(book, child))
return Markup(out.getvalue())
case None:
@ -66,3 +69,30 @@ def hexdoc_wrap(value: str, *args: str):
else:
attributes = ""
return Markup(f"<{tag}{attributes}>{Markup.escape(value)}</{tag}>")
# aliased as _() and _f() at render time
def hexdoc_localize(
key: str,
*,
do_format: bool,
props: Properties,
book: Book,
i18n: I18n,
allow_missing: bool,
):
# get the localized value from i18n
localized = i18n.localize(key, allow_missing=allow_missing)
if not do_format:
return Markup(localized.value)
# construct a FormatTree from the localized value (to allow using patchi styles)
formatted = FormatTree.format(
localized.value,
book_id=book.id,
i18n=i18n,
macros=book.macros,
is_0_black=props.is_0_black,
)
return Markup(hexdoc_block({"book": book}, formatted))

View file

@ -46,10 +46,8 @@ packages = [
]
[template.args]
title = "{{ cookiecutter.mod_name }} Book"
mod_name = "{{ cookiecutter.mod_name }}"
author = "{{ cookiecutter.author }}"
description = "The {{ cookiecutter.mod_name }} Book, all in one place."
icon_href = "icon.png"
show_landing_text = false

View file

@ -0,0 +1,8 @@
{
hexdoc: {
"{{ cookiecutter.modid }}": {
title: "{{ cookiecutter.mod_name }} Book",
description: "The {{ cookiecutter.mod_name }} Book, all in one place.",
},
}
}

View file

@ -97,6 +97,8 @@ include = ["doc/src"]
extraPaths = ["doc/src"]
exclude = ["doc/{{cookiecutter.directory}}"]
enableExperimentalFeatures = true
# mostly we use strict mode
# but pyright doesn't allow decreasing error severity in strict mode
# so we need to manually specify all of the strict mode overrides so we can do that :/