Initial i18n support

This commit is contained in:
cdn0x12 2026-05-21 05:56:12 +08:00
parent 0829806f6a
commit 56c41ae914
No known key found for this signature in database
GPG key ID: 0C656827F9F80080
11 changed files with 256 additions and 36 deletions

View file

@ -256,3 +256,39 @@ a.show-on-focus:focus {
.card-text p:last-child {
margin-bottom: 0;
}
/* language dropdown */
.lang-dropdown {
border-radius: var(--bs-border-radius);
border: 1px solid var(--bs-border-color-translucent);
padding: 4px;
}
.lang-dropdown .dropdown-item {
border-radius: var(--bs-border-radius);
padding: 6px 12px;
font-size: 14px;
}
.lang-dropdown .dropdown-item.active {
background-color: #0252cc;
}
[data-bs-theme="dark"] .lang-dropdown .dropdown-item.active,
.dark-mode .lang-dropdown .dropdown-item.active {
background-color: #66a3ff;
}
/* prevent right-overflow */
@media (max-width: 767.98px) {
.lang-dropdown {
right: 0;
left: auto;
}
}
/* remove right-side border radius */
.btn-group > .dropdown:first-child > .btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}

View file

@ -23,3 +23,29 @@ function toggleSidebar() {
sidebarEl.classList.add('show');
}
}
document.addEventListener('click', function (event) {
const dropdownToggle = event.target.closest('[data-bs-toggle="dropdown"]');
if (dropdownToggle) {
event.preventDefault();
const dropdownContainer = dropdownToggle.closest('.dropdown');
const dropdownMenu = dropdownContainer ? dropdownContainer.querySelector('.dropdown-menu') : null;
if (dropdownMenu) {
const isExpanded = dropdownToggle.getAttribute('aria-expanded') === 'true';
closeAllDropdowns();
dropdownToggle.setAttribute('aria-expanded', !isExpanded ? 'true' : 'false');
dropdownMenu.classList.toggle('show', !isExpanded);
}
} else {
closeAllDropdowns();
}
});
function closeAllDropdowns() {
document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(function (toggle) {
toggle.setAttribute('aria-expanded', 'false');
});
document.querySelectorAll('.dropdown-menu').forEach(function (menu) {
menu.classList.remove('show');
});
}

20
content/_data/i18n.json Normal file
View file

@ -0,0 +1,20 @@
{
"en": {
"docs": "Docs",
"toggle_dark_mode": "Toggle dark mode",
"close": "Close",
"filter_docs": "Filter docs",
"press_slash_to_focus": "Press [key] to focus",
"toggle_sidebar": "Toggle sidebar",
"view_history": "View History",
"view_source": "View Source",
"draft_notice": "Please note that this article is still a draft and might not have any contents yet.",
"copyright": "© Codeberg Docs Contributors. See [license]LICENSE[/license]",
"find_out_more": "Find out more in this section:",
"contributing_title": "Contributing",
"contributing_p1": "Hey there! 👋 Thank you for reading this article!",
"contributing_p2": "Is there something missing, or do you have an idea on how to improve the documentation? Do you want to write your own article?",
"contributing_p3": "You're invited to contribute to the Codeberg Documentation at [repo]its source code repository[/repo] for example, by [pr]adding a pull request[/pr] or joining in on the discussion in [issues]the issue tracker[/issues].",
"contributing_p4": "For an introduction on contributing to Codeberg Documentation, please have a look at [faq]the Contributor FAQ[/faq]."
}
}

View file

@ -0,0 +1,3 @@
{
"en": "English"
}

View file

@ -1,20 +1,25 @@
{% set i18nDict = i18n[lang or 'en'] %}
<div class="card" data-pagefind-ignore="all">
<div class="card-body">
<h5 class="card-title">Contributing</h5>
<h5 class="card-title">{{ i18nDict.contributing_title }}</h5>
<div class="card-text">
<p>Hey there! 👋 Thank you for reading this article!</p>
<p>{{ i18nDict.contributing_p1 }}</p>
<p>{{ i18nDict.contributing_p2 }}</p>
<p>
Is there something missing, or do you have an idea on how to improve the documentation?
Do you want to write your own article?
{{ i18nDict.contributing_p3 | tReplace({
"repo": '<a href="https://codeberg.org/Codeberg/Documentation">',
"/repo": '</a>',
"pr": '<a href="/collaborating/pull-requests-and-git-flow">',
"/pr": '</a>',
"issues": '<a href="https://codeberg.org/Codeberg/Documentation/issues">',
"/issues": '</a>'
}) | safe }}
</p>
<p>
You're invited to contribute to the Codeberg Documentation at <a href="https://codeberg.org/Codeberg/Documentation">its source code repository</a>
for example, by <a href="/collaborating/pull-requests-and-git-flow">adding a pull request</a>
or joining in on the discussion in <a href="https://codeberg.org/Codeberg/Documentation/issues">the issue tracker</a>.
</p>
<p>
For an introduction on contributing to Codeberg Documentation, please have a look
at <a href="https://docs.codeberg.org/improving-codeberg/docs-contributor-faq">the Contributor FAQ</a>.
{{ i18nDict.contributing_p4 | tReplace({
"faq": '<a href="https://docs.codeberg.org/improving-codeberg/docs-contributor-faq">',
"/faq": '</a>'
}) | safe }}
</p>
</div>
</div>

View file

@ -1,5 +1,8 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark" data-bs-core="modern">
{% set currentLang = lang or 'en' %}
{% set i18nDict = i18n[currentLang] %}
{% from "language_switcher.njk" import languageSwitcher %}
<html lang="{{ currentLang }}" data-bs-theme="dark" data-bs-core="modern">
<head>
<title>{% if eleventyNavigation.title %}{{ eleventyNavigation.title }} | {% endif %}Codeberg Documentation</title>
@ -84,17 +87,17 @@
width="24"
alt="Codeberg"
/>
Docs
{{ i18nDict.docs }}
</a>
<button
type="button"
class="btn btn-secondary btn-square d-none d-lg-inline-block"
aria-label="Toggle dark mode"
aria-label="{{ i18nDict.toggle_dark_mode }}"
onclick="toggleDarkMode()"
>
{% fas_icon "moon" %}
</button>
<button type="button" class="btn-close d-lg-none ms-1" aria-label="Close" onclick="toggleSidebar()"></button>
<button type="button" class="btn-close d-lg-none ms-1" aria-label="{{ i18nDict.close }}" onclick="toggleSidebar()"></button>
</div>
<div class="offcanvas-body position-relative p-0">
<div class="filter-docs sticky-top p-3">
@ -102,23 +105,21 @@
id="search-input"
type="text"
class="form-control search"
placeholder="Filter docs"
aria-label="Filter docs"
placeholder="{{ i18nDict.filter_docs }}"
aria-label="{{ i18nDict.filter_docs }}"
/>
<div class="mt-1">
<small
>Press
<kbd class="text-body" style="font-size: 10px; background-color: hsla(var(--bs-emphasis-color-hsl), 0.1)"
>/</kbd
>
to focus</small
>
<small>
{{ i18nDict.press_slash_to_focus | tReplace({
"key": '<kbd class="text-body" style="font-size: 10px; background-color: hsla(var(--bs-emphasis-color-hsl), 0.1)">/</kbd>'
}) | safe }}
</small>
</div>
</div>
<div class="p-3 pt-0">
<div id="search-results"></div>
<ul class="sidebar-nav list">
{% for entry in collections.all | eleventyNavigation %} {% set active = entry.url == page.url or entry.key ==
{% for entry in collections.all | filterByLang(currentLang) | eleventyNavigation %} {% set active = entry.url == page.url or entry.key ==
eleventyNavigation.parent %} {% if (not entry.draft) or active %}
<li class="position-relative mt-4 mb-1">
<h5 class="sidebar-header {% if active %} active{% endif %}">
@ -146,10 +147,10 @@
<nav class="navbar navbar-expand-md docs-navbar sticky-top">
<div class="container-md px-3 px-sm-4 px-xl-5 py-1 justify-content-start">
<div class="btn-group me-3 d-lg-none">
<button type="button" class="btn btn-secondary btn-square" aria-label="Toggle sidebar" onclick="toggleSidebar()">
<button type="button" class="btn btn-secondary btn-square" aria-label="{{ i18nDict.toggle_sidebar }}" onclick="toggleSidebar()">
{% fas_icon "bars" %}
</button>
<button type="button" class="btn btn-secondary btn-square" aria-label="Toggle dark mode" onclick="toggleDarkMode()">
<button type="button" class="btn btn-secondary btn-square" aria-label="{{ i18nDict.toggle_dark_mode }}" onclick="toggleDarkMode()">
{% fas_icon "moon" %}
</button>
</div>
@ -162,9 +163,10 @@
width="24"
alt="Codeberg"
/>
Docs
{{ i18nDict.docs }}
</a>
<div class="btn-group ms-auto d-md-none">
{{ languageSwitcher(true) }}
<a href="{{ urls.commitHistoryMaster }}/{{ page.inputPath }}" class="btn btn-secondary btn-square" target="_blank" rel="noreferrer">
{% fas_icon "history" %}
</a>
@ -174,13 +176,14 @@
</div>
<div class="collapse navbar-collapse" id="docs-navbar-collapse">
<div class="d-flex align-items-center justify-content-center ms-auto gap-2">
{{ languageSwitcher(false) }}
<a
href="{{ urls.commitHistoryMaster }}/{{ page.inputPath }}"
class="btn btn-secondary flex-grow-1 d-flex align-items-center flex-nowrap text-start position-relative"
target="_blank"
rel="noreferrer"
>
{% fas_icon "history" %} &nbsp; View History
{% fas_icon "history" %} &nbsp; {{ i18nDict.view_history }}
</a>
<a
href="{{ urls.docsSourcesMaster }}/{{ page.inputPath }}"
@ -188,7 +191,7 @@
target="_blank"
rel="noreferrer"
>
{% fas_icon "code" %} &nbsp; View Source
{% fas_icon "code" %} &nbsp; {{ i18nDict.view_source }}
</a>
</div>
</div>
@ -202,7 +205,7 @@
<h1 id="top" class="lh-base mb-1 content-title font-size-24">{{ eleventyNavigation.title }}</h1>
{% endif %} {% if eleventyNavigation.draft %}
<strong data-pagefind-ignore="all"
>Please note that this article is still a draft and might not have any contents yet.</strong
>{{ i18nDict.draft_notice }}</strong
>
{% endif %}
@ -219,8 +222,10 @@
<footer class="docs-footer" data-pagefind-ignore="all">
<div class="container-md px-3 px-sm-4 px-xl-5 py-3">
<p>
&copy; Codeberg Docs Contributors. See
<a href="https://codeberg.org/Codeberg/Documentation/src/branch/main/LICENSE.md">LICENSE</a>
{{ i18nDict.copyright | tReplace({
"license": '<a href="https://codeberg.org/Codeberg/Documentation/src/branch/main/LICENSE.md">',
"/license": '</a>'
}) | safe }}
</p>
</div>
</footer>

View file

@ -0,0 +1,46 @@
{% macro languageSwitcher(isMobile=false) %}
{% if languages and (languages | length) > 1 %}
<div class="dropdown">
<button
class="btn btn-secondary {% if isMobile %}btn-square{% else %}dropdown-toggle d-flex align-items-center gap-1{% endif %}"
type="button"
data-bs-toggle="dropdown"
data-bs-display="static"
aria-expanded="false"
{% if isMobile %}aria-label="Select Language"{% endif %}
>
{% fas_icon "globe" %}
{% if not isMobile %}
{{ languages[currentLang] or currentLang }}
{% endif %}
</button>
<ul class="dropdown-menu dropdown-menu-end lang-dropdown">
{% set alternateLinks = collections.all | customLocaleLinks(translationKey, currentLang) %}
{% for code, label in languages %}
{% if code == currentLang %}
<li><a class="dropdown-item active" href="#">{{ label }}</a></li>
{% else %}
{% set translated = false %}
{% set translatedUrl = "" %}
{% for link in alternateLinks %}
{% if link.lang == code %}
{% set translated = true %}
{% set translatedUrl = link.url %}
{% endif %}
{% endfor %}
{% if translated %}
<li><a class="dropdown-item" href="{{ translatedUrl }}">{{ label }}</a></li>
{% else %}
{% set rootUrl = "/" + code + "/" if code != "en" else "/" %}
<li>
<a class="dropdown-item text-muted d-flex align-items-center justify-content-between" href="{{ rootUrl }}">
{{ label }} <span class="badge text-bg-secondary ms-2" style="font-size: 10px;">Home</span>
</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
{% endmacro %}

View file

@ -1 +0,0 @@
{ "layout": "default_layout" }

View file

@ -0,0 +1,13 @@
export default {
layout: "default_layout",
lang: "en",
eleventyComputed: {
translationKey: (data) => {
let stem = data.page.filePathStem;
if (stem.endsWith("/index")) {
stem = stem.substring(0, stem.length - 6);
}
return stem.startsWith('/') ? stem.substring(1) : stem;
}
}
};

View file

@ -0,0 +1,27 @@
export default {
layout: "default_layout",
eleventyComputed: {
permalink: (data) => {
let stem = data.page.filePathStem;
if (stem.startsWith("/l10n/")) {
stem = stem.substring(5);
}
if (stem.endsWith("/index")) {
stem = stem.substring(0, stem.length - 5);
} else if (stem.endsWith("/home")) {
stem = stem.substring(0, stem.length - 4);
}
return stem + "/";
},
translationKey: (data) => {
let stem = data.page.filePathStem;
// extract segment after '/l10n/<lang>/'
let match = stem.match(/^\/l10n\/[^/]+\/(.*)$/);
let relative = match ? match[1] : stem;
if (relative.endsWith("/index")) {
relative = relative.substring(0, relative.length - 6);
}
return relative.startsWith('/') ? relative.substring(1) : relative;
}
}
};

View file

@ -8,6 +8,7 @@ import markdownItAnchor from 'markdown-it-anchor';
import { library, icon } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons';
import { execSync } from 'child_process';
import { EleventyI18nPlugin } from '@11ty/eleventy';
export default function (eleventyConfig) {
eleventyConfig.addPlugin(navigationPlugin);
@ -21,6 +22,41 @@ export default function (eleventyConfig) {
decoding: 'async',
},
});
eleventyConfig.addPlugin(EleventyI18nPlugin, {
defaultLanguage: 'en',
});
eleventyConfig.addFilter('filterByLang', function (collection, lang) {
return (collection || []).filter((item) => {
const itemLang = item.data?.lang || 'en';
return itemLang === lang;
});
});
eleventyConfig.addFilter('customLocaleLinks', function (collection, currentKey, currentLang) {
currentLang = currentLang || 'en';
if (!currentKey) return [];
return (collection || [])
.filter((item) => {
const itemKey = item.data?.translationKey;
const itemLang = item.data?.lang || 'en';
return itemKey === currentKey && itemLang !== currentLang;
})
.map((item) => ({
url: item.url,
lang: item.data?.lang || 'en',
}));
});
// tag replacement wrapper for i18n
eleventyConfig.addFilter("tReplace", function (str, replacements) {
if (!str) return "";
if (!replacements || typeof replacements !== 'object') return str;
let result = str;
for (const [key, value] of Object.entries(replacements)) {
const safeValue = value !== null && value !== undefined ? String(value) : "";
result = result.replaceAll(`[${key}]`, safeValue);
}
return result;
});
eleventyConfig.addPassthroughCopy('assets');
eleventyConfig.addPassthroughCopy('fonts');
@ -83,7 +119,7 @@ export default function (eleventyConfig) {
});
// the article list navigation for section index pages
eleventyConfig.addShortcode('sectionNav', function (collections) {
eleventyConfig.addShortcode('sectionNav', function (collections, lang) {
const navFilter = eleventyConfig.getFilter('eleventyNavigation');
// from the nav tree, find the current page's entry
@ -95,9 +131,13 @@ export default function (eleventyConfig) {
.map((child) => `<tr><td><a href="${child.url}">${child.title}</a></td></tr>`)
.join('');
const i18n = this.ctx?.i18n || {};
const currentLang = lang || this.ctx?.lang || 'en';
const headingText = i18n[currentLang]?.find_out_more || 'Find out more in this section:';
return `<table class="table">
<thead>
<th>Find out more in this section:</th>
<th>${headingText}</th>
</thead>
<tbody>${rows}</tbody>
</table>`;