Skip to content

Commit

Permalink
improved multilanguage plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
oscarotero committed Nov 28, 2023
1 parent 482f111 commit 0a033db
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 129 deletions.
1 change: 1 addition & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
## `multilanguage` Plugin

- Apply the default language to all pages without defined language.
- Removed the ability to insert translations in the middle of the data object.

## `sass` Plugin

Expand Down
12 changes: 12 additions & 0 deletions core/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ export interface RawData {

/** The data of a page/folder once loaded and processed */
export interface Data extends RawData {
/** The language of the page */
lang?: string;

/** The id of the page (used to join different pages as versions of the same content) */
id?: string | number;

/** List of tags assigned to a page or folder */
tags: string[];

Expand All @@ -215,4 +221,10 @@ export interface Data extends RawData {

/** The page reference */
page: Page;

/**
* Alternate pages (for languages)
* @see https://lume.land/plugins/multilanguage/
*/
alternates?: Data[];
}
157 changes: 34 additions & 123 deletions plugins/multilanguage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Page } from "../core/file.ts";
import { isPlainObject, merge } from "../core/utils/object.ts";
import { merge } from "../core/utils/object.ts";
import { log } from "../core/utils/log.ts";
import { getPageUrl } from "../core/utils/page_url.ts";
import { posix } from "../deps/path.ts";

import type Site from "../core/site.ts";
import type { Data } from "../core/file.ts";
Expand Down Expand Up @@ -33,14 +31,14 @@ export default function multilanguage(userOptions: Options) {

// Preprocessor to setup multilanguage pages
site.preprocess(options.extensions, (pages, allPages) => {
pages.forEach((page) => {
for (const page of pages) {
const { data } = page;
const languages = data.lang as string | string[] | undefined;

// If the "lang" variable is not defined, use the default language
if (languages === undefined) {
data.lang = options.defaultLanguage;
return;
continue;
}

// If the "lang" variable is a string, check if it's a valid language
Expand All @@ -50,105 +48,73 @@ export default function multilanguage(userOptions: Options) {
`[multilanguage plugin] The language "${languages}" in the page ${page.sourcePath} is not defined in the "languages" option.`,
);
}
return;
continue;
}

// The "lang" variable of the pages must be an array
if (!Array.isArray(languages)) {
return;
throw new Error(`Invalid "lang" variable in ${page.sourcePath}`);
}

// Check if it's a valid language
if (!options.languages.some((lang) => !languages.includes(lang))) {
log.warning(
`[multilanguage plugin] One or more languages in the page ${page.sourcePath} are not defined in the "languages" option.`,
);
continue;
}

// Create a new page per language
const newPages: Page[] = [];
const id: string = data.id || page.src.path.slice(1);
const basePath = posix.dirname(page.data.url);
const id = data.id ?? page.src.path.slice(1);

for (const lang of languages) {
const newData: Data = { ...data, lang, id };
const newPage = page.duplicate(undefined, newData);
newPages.push(newPage);

// Fix the url
const customUrl = (newData[`url.${lang}`] || newData[lang]?.url) as
| string
| undefined;

if (customUrl) {
newData.url = customUrl;
const url = getPageUrl(newPage, site.options.prettyUrls, basePath);
if (!url) {
log.warning(
`[multilanguage plugin] The page ${page.sourcePath} has a custom url "${customUrl}" that is not valid.`,
);
} else {
newData.url = url;
}
} else if (newData.url) {
newData.url = `/${lang}${newData.url}`;
}
}

// Replace the current page with the multiple language versions
allPages.splice(allPages.indexOf(page), 1, ...newPages);
});
}
});

// Preprocessor to process the multilanguage data
site.preprocess(options.extensions, (pages) => {
pages.forEach((page) => {
const lang = page.data.lang;
site.preprocess(options.extensions, (pages, allPages) => {
for (const page of pages) {
const data = page.data;
const { lang } = data;

if (typeof lang !== "string") {
return;
continue;
}

const data = filterLanguage(
options.languages,
lang,
page.data,
) as Data;

// Resolve the language data
for (const key of options.languages) {
if (key in data) {
// If the language is the default one, merge the data
if (key === lang) {
Object.assign(data, data[key]);
}
// Otherwise, delete the data
delete data[key];
}
}

page.data = data;
});
});

// Preprocessor to (un)prefix all urls with the language code
site.preprocess(options.extensions, (pages) => {
pages.forEach((page) => {
const { lang } = page.data;

if (typeof lang !== "string") {
return;
}

const url = page.data.url;

// Preprocessor to (un)prefix all urls with the language code
const { url } = data;
if (!url.startsWith(`/${lang}/`) && lang !== options.defaultLanguage) {
page.data.url = `/${lang}${url}`;
data.url = `/${lang}${url}`;
} else if (
url.startsWith(`/${lang}/`) && lang === options.defaultLanguage
data.url.startsWith(`/${lang}/`) && lang === options.defaultLanguage
) {
page.data.url = url.slice(lang.length + 1);
data.url = url.slice(lang.length + 1);
}
});
});

// Preprocessor to build the alternates object
site.preprocess(options.extensions, (pages, allPages) => {
pages.forEach((page) => {
const { data } = page;
const id = data.id as string | number | undefined;

if (data.alternates || !id) {
return;
// Create the alternates object if it doesn't exist
const { id } = data;
if (data.alternates || id === undefined) {
continue;
}

const alternates: Data[] = [];
Expand All @@ -162,7 +128,7 @@ export default function multilanguage(userOptions: Options) {
page.data.alternates = alternates;
}
});
});
}
});

// Include automatically the <link rel="alternate"> elements
Expand Down Expand Up @@ -195,58 +161,3 @@ export default function multilanguage(userOptions: Options) {
});
};
}

/**
* Remove the entries from all "langs" except the "lang" value
*/
function filterLanguage(
langs: string[],
lang: string,
data: Record<string, unknown>,
): Record<string, unknown> {
const result: Record<string, unknown> = {};

for (const [name, value] of Object.entries(data)) {
const parts = name.match(/^(.*)\.([^.]+)$/);

if (parts) {
const [, key, l] = parts;

if (lang === l) {
result[key] = value;
continue;
} else if (langs.includes(l)) {
continue;
}
}

if (name in result) {
continue;
}

if (isPlainObject(value)) {
result[name] = filterLanguage(langs, lang, value);
} else if (Array.isArray(value)) {
result[name] = value.map((item) => {
return isPlainObject(item) ? filterLanguage(langs, lang, item) : item;
});
} else {
result[name] = value;
}
}

return result;
}

/** Extends PageData interface */
declare global {
namespace Lume {
export interface PageData {
/**
* Alternate pages (for languages)
* @see https://lume.land/plugins/multilanguage/
*/
alternates: PageData[];
}
}
}
5 changes: 3 additions & 2 deletions tests/assets/multilanguage/index.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
lang: [en, gl]
title: Hello world
title.gl: Ola mundo
content.gl: Ola mundo
gl:
title: Ola mundo
content: Ola mundo
---

Hello world
2 changes: 1 addition & 1 deletion tests/assets/multilanguage/other.vto
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
lang: [en, fr]
title.fr: Autre
en:
title: Other
url: /other-page-english/
fr:
title: Autre
url: /other-page-french.html
---

Expand Down
3 changes: 0 additions & 3 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ declare global {
// deno-lint-ignore no-explicit-any
export interface PageData<Type extends Record<any, any> = any>
extends Data, Type {
/** The language(s) of the page */
lang?: string;

/** The title of the page */
title?: string;
}
Expand Down

0 comments on commit 0a033db

Please sign in to comment.