Merge branch 'master' of git.fsfe.org:FSFE/fsfe-website

This commit is contained in:
2025-12-18 09:39:12 +01:00
28 changed files with 284 additions and 96 deletions

View File

@@ -123,3 +123,24 @@ def get_basepath(file: Path) -> Path:
def get_basename(file: Path) -> str:
"""Return the name of the file with the last two suffixes removed."""
return file.with_suffix("").with_suffix("").name
def get_localised_file(base_file: Path, lang: str, suffix: str) -> Path | None:
"""Return basefile localised if exists, else fallback if exists, else none."""
# ensure the suffix has a leading .
normalised_suffix = "." + suffix.removeprefix(".")
return (
localised
if (
localised := base_file.with_suffix(
base_file.suffix + f".{lang}" + normalised_suffix
)
).exists()
else fallback
if (
fallback := base_file.with_suffix(
base_file.suffix + ".en" + normalised_suffix
)
).exists()
else None
)

View File

@@ -14,7 +14,11 @@ from typing import TYPE_CHECKING
from lxml import etree
from fsfe_website_build.lib.misc import get_basepath, sort_dict, update_if_changed
from fsfe_website_build.lib.misc import (
get_basepath,
get_localised_file,
update_if_changed,
)
if TYPE_CHECKING:
import multiprocessing.pool
@@ -24,62 +28,45 @@ logger = logging.getLogger(__name__)
def _write_localmenus(
source_dir: Path,
directory: str,
files_by_dir: dict[str, list[Path]],
directory: Path,
files: list[Path],
languages: list[str],
) -> None:
"""Write localmenus for a given directory."""
# Set of files with no langcode or xhtml extension
base_files = {get_basepath(filter_file) for filter_file in files_by_dir[directory]}
base_files = {get_basepath(file) for file in files}
for lang in languages:
file = Path(directory).joinpath(f".localmenu.{lang}.xml")
logger.debug("Creating %s", file)
localmenu_file = directory.joinpath(f".localmenu.{lang}.xml")
logger.debug("Creating %s", localmenu_file)
page = etree.Element("feed")
# Add the subelements
version = etree.SubElement(page, "version")
version.text = "1"
etree.SubElement(page, "version").text = "1"
for source_file in [
path
for path in (
base_file.with_suffix(f".{lang}.xhtml")
if base_file.with_suffix(f".{lang}.xhtml").exists()
else (
base_file.with_suffix(".en.xhtml")
if base_file.with_suffix(".en.xhtml").exists()
else None
)
for base_file in base_files
)
if path is not None
]:
for base_file in base_files:
# source file to get localmenu data from
source_file = get_localised_file(base_file, lang, ".xhtml")
if source_file is None:
logger.debug("No source for basefile %s", base_file)
continue
# the file we are linking to in the localmenu
link_file = base_file.with_suffix(base_file.suffix + ".html")
# now generate a localmenu entry for each localmenu entry in the source file
for localmenu in etree.parse(source_file).xpath("//localmenu"):
etree.SubElement(
page,
"localmenuitem",
set=(
str(localmenu.xpath("./@set")[0])
if localmenu.xpath("./@set") != []
else "default"
),
id=(
str(localmenu.xpath("./@id")[0])
if localmenu.xpath("./@id") != []
else "default"
),
set=localmenu.get("set", "default"),
id=localmenu.get("id", "default"),
link=(
"/"
+ str(
source_file.with_suffix(".html").relative_to(source_dir),
link_file.relative_to(source_dir),
)
),
).text = localmenu.text
update_if_changed(
file,
etree.tostring(page, encoding="utf-8").decode("utf-8"),
)
update_if_changed(localmenu_file, etree.tostring(page, encoding="unicode"))
def update_localmenus(
@@ -91,44 +78,38 @@ def update_localmenus(
"""Update all the .localmenu.*.xml files containing the local menus."""
logger.info("Updating local menus")
# Get a dict of all source files containing local menus
files_by_dir: dict[str, set[Path]] = defaultdict(set)
for file in filter(
lambda path: "-template" not in path.name,
source_dir.glob("**/*.??.xhtml"),
files_by_dir: dict[Path, list[Path]] = defaultdict(list)
for file in (
file
for file in source_dir.glob("**/*.??.xhtml")
if "-template" not in file.name
):
xslt_root = etree.parse(file)
if xslt_root.xpath("//localmenu"):
directory_xpath = xslt_root.xpath("//localmenu/@dir")
directory = str(
source.joinpath(directory_xpath[0])
if directory_xpath
else file.parent.resolve().relative_to(source.resolve())
for localmenu_elem in xslt_root.xpath("//localmenu"):
directory = Path(
localmenu_elem.get(
"dir", str(file.parent.resolve().relative_to(source.resolve()))
)
)
files_by_dir[directory].add(file)
files_by_dir = sort_dict(files_by_dir)
files_by_dir[directory].append(file)
# If any of the source files has been updated, rebuild all .localmenu.*.xml
dirs = filter(
lambda directory: (
any(
dirs = [
(directory, files)
for directory, files in files_by_dir.items()
if any(
[
(
(
(not Path(directory).joinpath(".localmenu.en.xml").exists())
or (
file.stat().st_mtime
> Path(directory)
.joinpath(".localmenu.en.xml")
.stat()
.st_mtime
)
)
for file in files_by_dir[directory]
),
)
),
files_by_dir,
)
not (
localmenu_path := directory.joinpath(".localmenu.en.xml")
).exists()
)
or (file.stat().st_mtime > localmenu_path.stat().st_mtime)
]
for file in files
)
]
pool.starmap(
_write_localmenus,
((source_dir, directory, files_by_dir, languages) for directory in dirs),
((source_dir, directory, files, languages) for directory, files in dirs),
)

View File

@@ -10,6 +10,7 @@ from fsfe_website_build.lib.misc import (
delete_file,
get_basename,
get_basepath,
get_localised_file,
get_version,
keys_exists,
lang_from_filename,
@@ -111,3 +112,68 @@ def get_basepath_test() -> None:
def get_basename_test() -> None:
assert get_basename(Path("a.b.c")) == "a"
assert get_basename(Path("a/b.c.d")) == "b"
def get_localised_file_localized__test(tmp_path: Path) -> None:
base_file = tmp_path / "test"
base_file.write_text("content")
localized_file = tmp_path / "test.fr.xhtml"
localized_file.write_text("french content")
result = get_localised_file(base_file, "fr", "xhtml")
assert result == localized_file
assert result is not None
assert result.exists()
def get_localised_file_localized_missing_test(
tmp_path: Path,
) -> None:
base_file = tmp_path / "test"
base_file.write_text("content")
fallback_file = tmp_path / "test.en.xhtml"
fallback_file.write_text("english content")
result = get_localised_file(base_file, "de", "xhtml")
assert result == fallback_file
assert result is not None
assert result.exists()
def get_localised_file_neither_exists_test(tmp_path: Path) -> None:
base_file = tmp_path / "test"
base_file.write_text("content")
result = get_localised_file(base_file, "fr", "xhtml")
assert result is None
def get_localised_file_existing_suffix_test(tmp_path: Path) -> None:
base_file = tmp_path / "test.suffix.test"
base_file.write_text("content")
localized_file = tmp_path / "test.suffix.test.fr.xml"
localized_file.write_text("xml content")
result = get_localised_file(base_file, "fr", "xml")
assert result == localized_file
assert result is not None
assert result.exists()
def get_localised_file_suffix_normalization_test(tmp_path: Path) -> None:
base_file = tmp_path / "test"
base_file.write_text("content")
localized_file = tmp_path / "test.fr.xml"
localized_file.write_text("xml content")
result = get_localised_file(base_file, "fr", "xml")
assert result == localized_file
assert result is not None
assert result.exists()
result2 = get_localised_file(base_file, "fr", ".xml")
assert result2 == localized_file

View File

@@ -24,6 +24,7 @@ RewriteRule ^graphics/fsfeurope.ico /graphics/fsfe.ico [R=301,L]
# Redirect
RewriteRule ^translate/?$ /contribute/translators/ [R=301,L]
RewriteRule ^apple/?$ /activities/apple-litigation/ [R=301,L]
RewriteRule ^at(ccc|CCC)/?$ /news/2025/news-20251214-01.html [R=302,L]
# Promotion material order
RewriteRule ^promo(/.*)? /contribute/spreadtheword$1 [R=301,L]

View File

@@ -8,7 +8,7 @@
<script src="/scripts/filter-teams.js"/>
</head>
<body>
<body class="ltr">
<p id="category"><a href="/about/">About</a></p>
<h1>The FSFE Team</h1>

View File

@@ -8,7 +8,7 @@
</head>
<body class="spreadtheword">
<body class="spreadtheword ltr">
<p id="category"><a href="/contribute/">Contribute</a></p>
<h1 id="spread-the-word">Spread the word!</h1>

View File

@@ -6,7 +6,7 @@
<title>Events</title>
</head>
<body class="toplevel article">
<body class="toplevel article ltr">
<h1>Events</h1>
<section id="add-event">

View File

@@ -6,7 +6,7 @@
<title>Announce your FSFE community event</title>
</head>
<body>
<body class="ltr">
<h1>Announce your FSFE community event</h1>
<div id="introduction">

View File

@@ -1278,3 +1278,7 @@ blockquote#statement p {
.special {
display: none;
}
.ltr {
direction: ltr;
}

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<html newsdate="2025-12-16">
<version>1</version>
<head>
<title>Wrapping Up the Year With Free Software at the 39C3</title>
</head>
<body>
<h1>Wrapping Up the Year With Free Software at the 39C3</h1>
<p>Once again, the FSFE is heading to the Chaos Communication Congress!
From 27 until the 30 December, we will be back in Hamburg as part of the
Bits &amp; Bäume assembly. Over 12.000 people will join us to
participate in this community event, full of great talks, workshops and
creatures from all over the world. Swing by, say hi, and warm up with
some Free Software vibes!</p>
<figure>
<img src="https://pics.fsfe.org/uploads/medium/d7/aa/cb70feb82ed93d9abfc1a6709cd3.jpeg"
alt="38c3 at Hamburg Messe in 2024: The building at night full of lights "/>
</figure>
<p>As the Christmas markets wind down, the Hamburg Congress Center
begins to transform. People arrive from all directions; spaceships and
blinking lights appear; hackerspaces from across Europe start setting up
their assemblies. Civil society groups gather to showcase their work and
their hopes for the future, while art installations rise piece by piece.
A low murmur fills the CCH, one that is growing louder and more vibrant
by the minute: an unmistakable sign that the Chaos Communication
Congress is coming back to life.</p>
<p>The 39th edition of this well-known event will once again take over
Hamburg from December 27 to 30, filling the post-Christmas lull with
creativity, tech, activism, and a whole lot of interesting people. This
conference, organised by and for the community, brings together around
12,000 participants each year, people who do not want to miss the chance
to be part of this utopic and ever-evolving event full of community,
curiosity, and digital freedom. For four days, the CCH becomes a
temporary city: a place where bold ideas are built overnight and where
the boundaries of technology, art, and society are constantly being
pushed and reimagined.</p>
<p>You will find the FSFE crew <a
href="https://events.ccc.de/congress/2025/hub/en/assembly/detail/bitsundbaeume_aboutfreedom">at
the Bits &amp; Bäume assembly</a> with a
booth with stickers and merchandise, as well as a full program
presenting our 2048 vision: a future in which everyone has the right to
remove and install any software on any of their devices, all public
funding for software is dedicated exclusively to Free Software,
regulatory frameworks actively encourage the use and development of Free
Software, licensing and legal decisions are grounded in facts rather
than fear, uncertainty, and doubt, and young people can tinker,
experiment, and learn to code with Free Software as the default. Come
by, meet us, and help us turn this vision into reality.</p>
<p>Of course, we will not be alone: plenty of other Bits &amp; Bäume
organisations will be right around us, bringing brilliant talks, fun
workshops, and lively booths. And do not forget the hackerspaces and the
official program! With so many exciting things happening around the
clock for four full days, the real challenge will be choosing where to
go next.</p>
<p>So, <a href="/events/index.html#event-20251227-01">what to expect
from the FSFE at 39C3?</a> We are planning to have a
dedicated FSFE contributors and volunteers meeting, <a
href="http://ada.fsfe.org/">“Ada &amp; Zangemann”</a> reading, hands-on
workshops about
<a href="http://reuse.software">REUSE</a> and about running an
organisation with Free Software, a deep dive
into <a href="/activities/deviceneutrality/index.html">Device
Neutrality</a> and the <a
href="/activities/apple-litigation/apple-litigation.html">Apple v. EU
litigation</a>, and of course we
will be answering your questions about our Vision 2048 and current
activities such as <a href="http://yh4f.org/">Youth Hacking 4 Freedom</a>, which is open for
registration and will start directly after the 39C3 is over.</p>
<p>But, most importantly bring your instruments and yourself to our
booth for our daily 19:00h Free Software sing-along. So, stop by, warm
up, say hello, and share some Free Software holiday cheer with us. </p>
<p>
We are looking forward to seeing you at <a
href="https://events.ccc.de/congress/2025/infos/startpage.html">39C3!</a>
</p>
</body>
<tags>
<tag key="news">News</tag>
<tag key="front-page"/>
<tag key="reuse">REUSE</tag>
<tag key="policy">Policy</tag>
<tag key="pmpc">Public Money? Public Code!</tag>
<tag key="dma">DMA</tag>
<tag key="ccc">Chaos Communication Congress</tag>
<tag key="ngi">Next Generation Internet</tag>
<tag key="ada-zangemann">Ada and Zangemann</tag>
<tag key="fediverse">Fediverse</tag>
<tag key="deviceneutrality">Device Neutrality</tag>
<tag key="legal">Legal</tag>
<tag key="merchandise">Merchandise</tag>
<tag key="community">Community</tag>
<tag key="de">Germany</tag>
<tag key="fya">Free Your Android</tag>
<tag key="yh4f">Youth Hacking 4 Freedom</tag>
<tag key="windows-tax">Windows refund</tag>
<tag key="highlights">Highlights</tag>
</tags>
<discussion href="https://mastodon.social/deck/@fsfe/115729419639756759"/>
<image url="https://pics.fsfe.org/uploads/medium/d7/aa/cb70feb82ed93d9abfc1a6709cd3.jpeg"
alt="38c3 at Hamburg Messe in 2024: The building at night full of lights "/>
</html>

View File

@@ -6,7 +6,7 @@
<title>News</title>
</head>
<body class="toplevel news-index">
<body class="toplevel news-index ltr">
<h1>News</h1>
<div id="introduction">

View File

@@ -8,7 +8,7 @@
<title>Newsletters</title>
</head>
<body>
<body class="ltr">
<h1>Newsletters</h1>
<p id="introduction">
In addition to our <a href="news.html">regular news stories</a>, each

View File

@@ -6,7 +6,7 @@
<title>Software Freedom Podcast</title>
</head>
<body class="news-index">
<body class="news-index ltr">
<!-- Breadcumb -->
<p id="category"><a href="news.html">News</a></p>
<!-- / Breadcumb -->

View File

@@ -6,7 +6,7 @@
<title>Merchandise</title>
</head>
<body class="toplevel">
<body class="toplevel ltr">
<h1>Merchandise</h1>
<module id="order-delay"/>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<div class="banner-become-supporter">
<div class="banner-become-supporter ltr">
<p>
Our intervention requires sustainable funding.
</p>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<div class="banner-become-contributor">
<div class="banner-become-contributor ltr">
<p>
Join us in our work for software freedom.
</p>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<div class="banner-become-supporter">
<div class="banner-become-supporter ltr">
<p>
Freedom in the information society needs your financial contribution.
</p>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<div class="banner-become-translator">
<div class="banner-become-translator ltr">
<p>
Help to spread the word about Free Software!
</p>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<div class="banner-become-supporter">
<div class="banner-become-supporter ltr">
<p>
Become an FSFE supporter and help us fight for Router Freedom in Europe!
</p>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<div class="banner-subscribe-box">
<div class="banner-subscribe-box ltr">
<p>
Subscribe to our email updates. Our experts inform you about current
news, events, activities, and how you can contribute. <span style="font-size:0.8em">(Our <a href="/about/legal/imprint.html#id-privacy-policy"><span style="color:white; text-decoration: underline;">Privacy Policy</span></a>)</span>

View File

@@ -3,7 +3,7 @@
<version>3</version>
<module>
<div class="banner-subscribe">
<div class="banner-subscribe ltr">
<p>
Subscribe to our email updates. Our experts inform you about current
news, events, activities, and how you can contribute. <span style="font-size:0.8em">(Our <a href="/about/legal/imprint.html#id-privacy-policy"><span style="color:white; text-decoration: underline;">Privacy Policy</span></a>)</span>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<div class="banner-become-supporter">
<div class="banner-become-supporter ltr">
<p>
Software Freedom needs your financial contribution.
</p>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<div class="module-follow-news">
<div class="module-follow-news ltr">
<h2>Follow our News</h2>
<p>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<p class="alert alert-success">
<p class="alert alert-success ltr">
<strong>SUMMER SALE!</strong> No shipping costs to all orders from 6 euros until 30 June!
</p>
</module>

View File

@@ -3,7 +3,7 @@
<version>1</version>
<module>
<p class="alert alert-danger">
<p class="alert alert-danger ltr">
At the moment we cannot process promotion material and merchandise orders
at the usual speed. Anyway, our staffers are doing their best to satisfy
all your requests as quickly as possible. Thank you for your understanding

View File

@@ -3,7 +3,7 @@
<version>2</version>
<module>
<div class="module-social-media">
<div class="module-social-media ltr">
<h2>Social Media</h2>
<p>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<data>
<version>6</version>
<div class="text-button-box">
<div class="text-button-box" style="direction: ltr;">
<div class="row">
<div class="col-xs-12 col-sm-9 col-md-10">
<p>

View File

@@ -34,13 +34,10 @@ ENV PATH="$UV_PROJECT_ENVIRONMENT/bin:$PATH"
# Set the workdir
WORKDIR /website-source-during-build
# Copy the pyproject and build deps
# Done in a seperate step for optimal docker caching
COPY ./pyproject.toml ./uv.lock ./
RUN uv sync --no-install-package fsfe_website_build --group dev
# Copy pyproject, build deps & entrypoint
COPY ./pyproject.toml ./uv.lock pre-commit.entrypoint.sh ./
# Copy entrypoint
COPY pre-commit.entrypoint.sh ./
RUN uv sync --no-install-package fsfe_website_build --group dev
# Set the workdir
WORKDIR /website-source