Skip to content

Commit

Permalink
Use different ThemeConfig for each locale (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
its-miroma authored Jun 12, 2024
1 parent bb71781 commit 12a367f
Show file tree
Hide file tree
Showing 21 changed files with 784 additions and 654 deletions.
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 localeDir - The directory of the locale (null for English).
*/
function getNavbarResolver(localeDir: 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(localeDir == null) {
translations = fallbackTranslations
} else {
if(!existsSync(resolve("translated", localeDir, "navbar_translations.json"))) {
translations = fallbackTranslations;
} else {
translations = JSON.parse(readFileSync(resolve("translated", localeDir, "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

0 comments on commit 12a367f

Please sign in to comment.