Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use different ThemeConfig for each locale #100

Merged
merged 7 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 10 additions & 68 deletions .vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { PageData, TransformPageContext } from 'vitepress'

import defineVersionedConfig from 'vitepress-versioning-plugin'
import snippetPlugin from 'markdown-it-vuepress-code-snippet-enhanced'
import defineVersionedConfig, { VersionedConfig } from 'vitepress-versioning-plugin'

import PlayersSidebar from './sidebars/players'
import DevelopSidebar from "./sidebars/develop"

import { applySEO } from './seo'
import { removeVersionedItems } from "./seo"
import { loadLocales, generateTranslatedSidebars } from './i18n'
import { loadLocales } from './i18n'
import { applySEO, removeVersionedItems } from './seo'

// https://vitepress.dev/reference/site-config
// https://www.npmjs.com/package/vitepress-versioning-plugin
Expand All @@ -21,27 +17,19 @@ export default defineVersionedConfig(__dirname, {
},

rewrites: {
// Ensures that it's `/contributing` instead of `/CONTRIBUTING`.
'(.*)CONTRIBUTING.md': '(.*)contributing.md',
'translated/:lang/(.*)': ':lang/(.*)'
},

title: "Fabric Documentation",
description: "Comprehensive documentation for Fabric, the Minecraft modding toolchain.",
cleanUrls: true,

head: [
['link', { rel: 'icon', sizes: '32x32', href: '/favicon.png' }],
],

locales: {
root: {
label: 'English',
lang: 'en'
},
head: [[
'link',
{ rel: 'icon', sizes: '32x32', href: '/favicon.png' }
]],

...loadLocales(__dirname)
},
locales: loadLocales(__dirname),

// Prevent dead links from being reported as errors - allows partially translated pages to be built.
ignoreDeadLinks: true,
Expand All @@ -51,7 +39,7 @@ export default defineVersionedConfig(__dirname, {
"LICENSE.md",
],

transformPageData(pageData: PageData, ctx: TransformPageContext) {
transformPageData(pageData: PageData, _ctx: TransformPageContext) {
applySEO(pageData);
},

Expand All @@ -68,51 +56,5 @@ export default defineVersionedConfig(__dirname, {
config(md) {
md.use(snippetPlugin);
}
},

themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: 'https://fabricmc.net/' },
{ text: 'Download', link: 'https://fabricmc.net/use' },
{
text: 'Contribute', items: [
// Expand on this later, with guidelines for loader+loom potentially?
{
text: 'Fabric Documentation',
link: '/contributing'
},
{
text: 'Fabric API',
link: 'https://github.com/FabricMC/fabric/blob/1.20.4/CONTRIBUTING.md'
}
]
},
],

search: {
provider: 'local'
},

outline: "deep",

sidebar: generateTranslatedSidebars(__dirname, {
'/players/': PlayersSidebar,
'/develop/': DevelopSidebar,
}),

editLink: {
pattern: ({ filePath }) => {
return `https://github.com/FabricMC/fabric-docs/edit/main/${filePath}`
},
text: 'Edit this page on GitHub'
},

socialLinks: [
{ icon: 'github', link: 'https://github.com/FabricMC/fabric-docs' },
{ icon: 'discord', link: 'https://discord.gg/v6v4pMv' }
],

logo: "/logo.png"
}
})
} as VersionedConfig)
246 changes: 166 additions & 80 deletions .vitepress/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,116 +1,202 @@
import { existsSync, readdirSync, readFileSync } from "fs";
import { resolve } from "path/posix";
import { ExtendedSidebarItem } from "./sidebars/utils";
import { DefaultTheme, LocaleConfig } from "vitepress";
import DevelopSidebar from "./sidebars/develop";
import PlayersSidebar from './sidebars/players';
import { ExtendedSidebarItem } from "./sidebars/utils";

export function applyTranslations(locale: string, translationSource: { [key: string]: string; }, fallbackSource: { [key: string]: string }, sidebar: ExtendedSidebarItem[]): ExtendedSidebarItem[] {
const sidebarCopy = JSON.parse(JSON.stringify(sidebar));

for (const item of sidebarCopy) {
if (item.disableTranslation) continue;

if (!translationSource[item.text]) {
if (fallbackSource[item.text]) {
item.text = fallbackSource[item.text];
}
} else {
item.text = translationSource[item.text];
}
/**
* Loads locales and generates a LocaleConfig object.
*
* @param rootDir - The root directory of the project.
* @returns A LocaleConfig object with locales and their corresponding themeConfig.
*/
export function loadLocales(rootDir: string): LocaleConfig<DefaultTheme.Config> {
const translatedFolder = resolve(rootDir, "..", "translated");
const translatedFolders = readdirSync(translatedFolder, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);

if (item.link && locale !== "en_us" && item.process !== false) {
// Prefix the link with the locale
item.link = `/${locale}${item.link}`;
const locales: LocaleConfig<DefaultTheme.Config> = {
root: {
label: 'English',
lang: 'en',
themeConfig: generateTranslatedThemeConfig(null)
}
};

if (item.items) {
item.items = applyTranslations(locale, translationSource, fallbackSource, item.items);
for (const folder of translatedFolders) {
if (!existsSync(resolve(translatedFolder, folder, "index.md"))) {
continue;
}
}

return sidebarCopy;
}
let firstHalf: string = folder.slice(0, 2);
let secondHalf: string = folder.slice(3, 5);

export function generateTranslatedSidebars(_rootDir: string, sidebars: { [url: string]: ExtendedSidebarItem[]; }): { [localeUrl: string]: ExtendedSidebarItem[]; } {
const sidebarResult = {};
let locale = new Intl.DisplayNames([`${firstHalf}-${secondHalf.toUpperCase()}`], { type: 'language' });
let localeName = locale?.of(`${firstHalf}-${secondHalf.toUpperCase()}`)!;

const englishFallbacks = JSON.parse(readFileSync(resolve(_rootDir, "..", "sidebar_translations.json"), "utf-8"));
// Capitalize the first letter of the locale name
localeName = localeName.charAt(0).toUpperCase() + localeName.slice(1);

// Create the default english sidebar.
for (const sidebarPair of Object.entries(sidebars)) {
const [url, sidebar] = sidebarPair;
sidebarResult[url] = applyTranslations("en_us", englishFallbacks, englishFallbacks, sidebar);
locales[folder] = {
label: localeName,
link: `/${folder}/`,
lang: folder,
themeConfig: generateTranslatedThemeConfig(folder),
}
}

const translatedFolder = resolve(_rootDir, "..", "translated");
return locales;
}

// Get all folder names from the translated folder
const translatedFolders = readdirSync(translatedFolder, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
/**
* Returns a resolving function for any navbar strings.
* @param localDir - The directory of the locale (null for English).
*/
function getNavbarResolver(localDir: string | null): (key: string) => string {
// Load navbar_translations.json of locale and english.
const fallbackTranslations = JSON.parse(readFileSync(resolve(__dirname, "..", "navbar_translations.json"), "utf-8"));
let translations: any;

if(localDir == null) {
translations = fallbackTranslations
} else {
if(!existsSync(resolve("translated", localDir, "navbar_translations.json"))) {
translations = fallbackTranslations;
} else {
translations = JSON.parse(readFileSync(resolve("translated", localDir, "navbar_translations.json"), "utf-8"));
}
}

for (const folder of translatedFolders) {
const sidebarPath = resolve(translatedFolder, folder, "sidebar_translations.json")
const indexPath = resolve(translatedFolder, folder, "index.md")
return (key: string) => {
return translations[key] ?? fallbackTranslations[key] ?? key;
}
}

if (!existsSync(indexPath)) {
continue;
}
/**
* Generates a theme configuration for a given locale.
*
* @param localeDir - The directory of the locale (null for English).
* @returns A theme configuration object.
*/
function generateTranslatedThemeConfig(localeDir: string | null): DefaultTheme.Config {
const navbarResolver = getNavbarResolver(localeDir);

return {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: navbarResolver('home'), link: 'https://fabricmc.net/' },
{ text: navbarResolver('download'), link: 'https://fabricmc.net/use' },
{
text: navbarResolver('contribute'), items: [
// TODO: Expand on this later, with guidelines for loader+loom potentially?
{
text: navbarResolver('title'),
link: `${localeDir ? `/${localeDir}` : ''}/contributing`
},
{
text: navbarResolver('contribute.api'),
link: 'https://github.com/FabricMC/fabric/blob/1.20.4/CONTRIBUTING.md'
}
]
},
],

// TODO: localise version switcher

search: {
provider: 'local'
},

outline: "deep",

sidebar: generateTranslatedSidebars(__dirname, {
'/players/': PlayersSidebar,
'/develop/': DevelopSidebar,
}),

editLink: {
pattern: ({ filePath }) => {
return `https://github.com/FabricMC/fabric-docs/edit/main/${filePath}`
},
text: localeDir ? readTranslations(resolve(__dirname, localeDir))['github.edit'] : 'Edit this page on GitHub'
},

socialLinks: [
{ icon: 'github', link: 'https://github.com/FabricMC/fabric-docs' },
{ icon: 'discord', link: 'https://discord.gg/v6v4pMv' }
],

logo: "/logo.png",

siteTitle: navbarResolver('title')
};
}

// If sidebar translations dont exist, use english fallback.
if (!existsSync(sidebarPath)) {
for (const sidebarPair of Object.entries(sidebars)) {
const [url, sidebar] = sidebarPair;
sidebarResult[`/${folder}${url}`] = sidebarResult[url];
/**
* Generates translated sidebars for a given root directory and sidebars.
*
* @param rootDir - The root directory to generate translated sidebars for.
* @param sidebars - An object containing sidebars to translate, keyed by URL.
* @returns An object containing translated sidebars, keyed by locale URL.
*/
function generateTranslatedSidebars(rootDir: string, sidebars: { [url: string]: ExtendedSidebarItem[] }): { [localeUrl: string]: ExtendedSidebarItem[] } {
function applyTranslations(
locale: string,
translationSource: { [key: string]: string },
fallbackSource: { [key: string]: string },
sidebar: ExtendedSidebarItem[]
): ExtendedSidebarItem[] {
const sidebarCopy = JSON.parse(JSON.stringify(sidebar));

for (const item of sidebarCopy) {
if (item.disableTranslation) continue;

item.text = translationSource[item.text] ?? fallbackSource[item.text];

if (item.link && locale !== "en_us" && item.process !== false) {
item.link = `/${locale}${item.link}`;
}

continue;
if (item.items) {
item.items = applyTranslations(locale, translationSource, fallbackSource, item.items);
}
}

const translations: { [key: string]: string; } = JSON.parse(readFileSync(sidebarPath, "utf-8"));

for (const sidebarPair of Object.entries(sidebars)) {
const [url, sidebar] = sidebarPair;

sidebarResult[`/${folder}${url}`] = applyTranslations(folder, translations, englishFallbacks, sidebar);
}
return sidebarCopy;
}

return sidebarResult;
}

export function loadLocales(_rootDir: string): LocaleConfig<DefaultTheme.Config> {
const translatedFolder = resolve(_rootDir, "..", "translated");

// Get all folder names from the translated folder
const englishFallbacks = readTranslations(resolve(rootDir, ".."));
const translatedFolder = resolve(rootDir, "..", "translated");
const translatedFolders = readdirSync(translatedFolder, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);

const locales: LocaleConfig<DefaultTheme.Config> = {};
const translatedSidebars: { [localeUrl: string]: ExtendedSidebarItem[] } = {};

for (const folder of translatedFolders) {
const indexPath = resolve(translatedFolder, folder, "index.md")
for (const sidebarPair of Object.entries(sidebars)) {
const [url, sidebar] = sidebarPair;
translatedSidebars[url] = applyTranslations("en_us", englishFallbacks, englishFallbacks, sidebar);
}

// Dont add language if index.md does not exist
if (!existsSync(indexPath)) {
continue;
for (const folder of translatedFolders) {
const translations = readTranslations(resolve(translatedFolder, folder));
for (const sidebarPair of Object.entries(sidebars)) {
const [url, sidebar] = sidebarPair;
translatedSidebars[`/${folder}${url}`] = applyTranslations(folder, translations, englishFallbacks, sidebar);
}
}

let firstHalf: string = folder.slice(0, 2);
let secondHalf: string = folder.slice(3, 5);

let locale = new Intl.DisplayNames([`${firstHalf}-${secondHalf.toUpperCase()}`], { type: 'language' });
let localeName = locale?.of(`${firstHalf}-${secondHalf.toUpperCase()}`)!;

// Capitalize the first letter of the locale name
localeName = localeName.charAt(0).toUpperCase() + localeName.slice(1);
return translatedSidebars;
}

locales[folder] = {
label: localeName,
link: `/${folder}/`,
lang: folder,
}
function readTranslations(folder: string | null): { [key: string]: string } {
folder ??= resolve(__dirname, '..');
const sidebarPath = resolve(folder, "sidebar_translations.json");
if (!existsSync(sidebarPath)) {
return readTranslations(null);
}

return locales;
return JSON.parse(readFileSync(sidebarPath, "utf-8"));
}
Loading