feat: Maintain Novel Updates Plugin With New Sources (#1952)

* Add GreenzTL

* fix

* fix

* Version update

* fix

* add debug

* Improve Wordpress and Blogspot detection

* further updates

* Added Dreamy Translations

* fix

* fix

* fix

* fix

* test

* Add Novels Hub

* Revert versions
This commit is contained in:
Batorian
2026-01-10 07:02:43 +01:00
committed by GitHub
parent 0064125421
commit 58da7bd1a5
2 changed files with 323 additions and 222 deletions
+245 -172
View File
@@ -6,7 +6,7 @@ import { Plugin } from '@/types/plugin';
class NovelUpdates implements Plugin.PluginBase { class NovelUpdates implements Plugin.PluginBase {
id = 'novelupdates'; id = 'novelupdates';
name = 'Novel Updates'; name = 'Novel Updates';
version = '0.9.4'; version = '0.9.5';
icon = 'src/en/novelupdates/icon.png'; icon = 'src/en/novelupdates/icon.png';
customCSS = 'src/en/novelupdates/customCSS.css'; customCSS = 'src/en/novelupdates/customCSS.css';
site = 'https://www.novelupdates.com/'; site = 'https://www.novelupdates.com/';
@@ -196,43 +196,6 @@ class NovelUpdates implements Plugin.PluginBase {
} }
} }
// Last edited in 0.9.0 by Batorian - 19/03/2025 // Last edited in 0.9.0 by Batorian - 19/03/2025
case 'anotivereads': {
chapterTitle = loadedCheerio('#comic-nav-name').first().text();
chapterContent = loadedCheerio('#spliced-comic').html()!;
break;
}
// Last edited in 0.9.0 by Batorian - 19/03/2025
case 'arcanetranslations': {
bloatElements = ['.bottomnav'];
bloatElements.forEach(tag => loadedCheerio(tag).remove());
chapterTitle = loadedCheerio('.epwrapper .cat-series').first().text();
loadedCheerio('.entry-content div, .entry-content span').each(
(_, element) => {
const el = loadedCheerio(element);
const style = el.attr('style');
if (!style) return; // Skip elements without inline styles
if (/border:.*#00219b/.test(style)) {
el.removeAttr('style').addClass('arcane_box_blue'); // Blue box
} else if (/border:.*white/.test(style)) {
el.removeAttr('style').addClass('arcane_box_white'); // White box
} else if (
style.includes('text-transform: uppercase') &&
/text-shadow:.*blue/.test(style)
) {
el.removeAttr('style').addClass('arcane_title_blue'); // Blue title
} else if (/text-shadow:.*blue/.test(style)) {
el.removeAttr('style').addClass('arcane_text_blue'); // Blue text
} else if (/text-shadow:.*lightyellow/.test(style)) {
el.removeAttr('style').addClass('arcane_text_lightyellow'); // Lightyellow text
} else if (/color:.*#ff00ff/.test(style)) {
el.removeAttr('style').addClass('arcane_text_pink'); // Pink text
}
},
);
chapterContent = loadedCheerio('.entry-content').html()!;
break;
}
// Last edited in 0.9.0 by Batorian - 19/03/2025
case 'asuratls': { case 'asuratls': {
const titleElement = loadedCheerio('.post-body div b').first(); const titleElement = loadedCheerio('.post-body div b').first();
chapterTitle = titleElement.text(); chapterTitle = titleElement.text();
@@ -348,6 +311,11 @@ class NovelUpdates implements Plugin.PluginBase {
chapterContent = loadedCheerio('.chapter__content').html()!; chapterContent = loadedCheerio('.chapter__content').html()!;
break; break;
} }
// Last edited in 0.9.5 by Batorian - 26/12/2025
case 'dreamy-translations': {
chapterTitle = loadedCheerio('h1 > span').first().text();
chapterContent = loadedCheerio('.chapter-content').html()!;
}
// Last edited in 0.9.0 by Batorian - 19/03/2025 // Last edited in 0.9.0 by Batorian - 19/03/2025
case 'fictionread': { case 'fictionread': {
bloatElements = [ bloatElements = [
@@ -411,22 +379,24 @@ class NovelUpdates implements Plugin.PluginBase {
} }
break; break;
} }
// Last edited in 0.9.0 by Batorian - 19/03/2025 // Last edited in 0.9.5 by Batorian - 26/12/2025
case 'helscans': { case 'greenztl': {
chapterTitle = loadedCheerio('.entry-title-main').first().text(); const chapterSlug = chapterPath.split('/').pop();
const chapterString_helscans = const apiUrl = `https://greenztl.com/api/chapters/slug/${chapterSlug}`;
'Chapter ' + chapterTitle.split('Chapter')[1].trim();
loadedCheerio('#readerarea.rdminimal') try {
.children() const response = await fetchApi(apiUrl);
.each((_, el) => { const json = await response.json();
const elementText = loadedCheerio(el).text();
if (elementText.includes(chapterString_helscans)) { const chapterName = json.data.name;
chapterTitle = elementText; const chapterNumber = json.data.chapterNumber;
loadedCheerio(el).remove(); const chapterCheerio = parseHTML(json.data.content);
return false;
} chapterTitle = `Chapter ${chapterNumber} - ${chapterName}`;
}); chapterContent = chapterCheerio.html()!;
chapterContent = loadedCheerio('#readerarea.rdminimal').html()!; } catch (error) {
throw new Error(`Failed to parse GreenzTL chapter: ${error}`);
}
break; break;
} }
// Last edited in 0.9.0 by Batorian - 19/03/2025 // Last edited in 0.9.0 by Batorian - 19/03/2025
@@ -441,7 +411,7 @@ class NovelUpdates implements Plugin.PluginBase {
chapterContent = loadedCheerio('#chapter-content').html()!; chapterContent = loadedCheerio('#chapter-content').html()!;
break; break;
} }
// Last edited in 0.9.0 by Batorian - 19/03/2025 // Last edited in 0.9.5 by Batorian - 26/12/2025
case 'infinitenoveltranslations': { case 'infinitenoveltranslations': {
// Get the chapter link from the main page // Get the chapter link from the main page
const url = loadedCheerio('article > p > a').first().attr('href')!; const url = loadedCheerio('article > p > a').first().attr('href')!;
@@ -450,8 +420,8 @@ class NovelUpdates implements Plugin.PluginBase {
const body = await response.text(); const body = await response.text();
loadedCheerio = parseHTML(body); loadedCheerio = parseHTML(body);
} }
chapterContent = loadedCheerio('.hentry').html()!; chapterContent = loadedCheerio('.entry-content').html()!;
chapterTitle = loadedCheerio('.page-entry-title').text(); chapterTitle = loadedCheerio('.entry-title').text();
break; break;
} }
// Last edited in 0.9.0 by Batorian - 19/03/2025 // Last edited in 0.9.0 by Batorian - 19/03/2025
@@ -537,6 +507,80 @@ class NovelUpdates implements Plugin.PluginBase {
chapterContent = loadedCheerio('.halChap--kontenInner ').html()!; chapterContent = loadedCheerio('.halChap--kontenInner ').html()!;
break; break;
} }
case 'novelshub': {
const segments = chapterPath.split('/');
const novelSlug = segments[segments.length - 2];
const chapterSlug = segments[segments.length - 1];
const apiUrl = `https://api.novelshub.org/api/chapter?mangaslug=${novelSlug}&chapterslug=${chapterSlug}`;
try {
const response = await fetchApi(apiUrl);
const json = await response.json();
const chapterNumber = json.chapter.number;
const chapterCheerio = parseHTML(json.chapter.content);
chapterTitle = `Chapter ${chapterNumber}`;
chapterCheerio('div').each((_, element) => {
const el = chapterCheerio(element);
const style = el.attr('style');
if (!style) return; // Skip elements without inline styles
// Orange box
if (/border:.*#ff6b00/.test(style)) {
el.removeAttr('style').addClass('novels-hub_box_orange');
// Orange box title
} else if (
/color:.*#ff6b00.*text-transform:.*uppercase/.test(style)
) {
el.removeAttr('style').addClass('novels-hub_box-title_orange');
// Orange box text
} else if (/color:.*white.*border-top:.*#ff6b00/.test(style)) {
el.removeAttr('style').addClass('novels-hub_box-text_orange');
// Green box
} else if (/border:.*#00ff88/.test(style)) {
el.removeAttr('style').addClass('novels-hub_box_green');
// Green box title
} else if (
/color:.*#00ff88.*text-transform:.*uppercase/.test(style)
) {
el.removeAttr('style').addClass('novels-hub_box-title_green');
// Green comment
} else if (/border-left:.*#00ff88/.test(style)) {
el.removeAttr('style').addClass('novels-hub_comment_green');
// Blue box
} else if (/border:.*#0066ff/.test(style)) {
el.removeAttr('style').addClass('novels-hub_box_blue');
// Blue box title
} else if (
/color:.*#0099ff.*text-transform:.*uppercase/.test(style)
) {
el.removeAttr('style').addClass('novels-hub_box-title_blue');
// Blue box text
} else if (/color:.*#d0d0d0/.test(style)) {
el.removeAttr('style').addClass('novels-hub_box-text_blue');
}
});
chapterCheerio('span').each((_, element) => {
const el = chapterCheerio(element);
const style = el.attr('style');
if (!style) return; // Skip elements without inline styles
// Red text
if (/color:.*#ff6b6b/.test(style)) {
el.removeAttr('style').addClass('novels-hub_text_red');
// Blue text
} else if (/color:.*#4d9fff/.test(style)) {
el.removeAttr('style').addClass('novels-hub_text_blue');
// Purple text
} else if (/color:.*#a78bfa/.test(style)) {
el.removeAttr('style').addClass('novels-hub_text_purple');
}
});
chapterContent = chapterCheerio.html()!;
} catch (error) {
throw new Error(`Failed to parse GreenzTL chapter: ${error}`);
}
break;
}
// Last edited in 0.9.0 by Batorian - 19/03/2025 // Last edited in 0.9.0 by Batorian - 19/03/2025
case 'novelworldtranslations': { case 'novelworldtranslations': {
bloatElements = ['.separator img']; bloatElements = ['.separator img'];
@@ -779,19 +823,6 @@ class NovelUpdates implements Plugin.PluginBase {
chapterText = await fetchApi(json).then(r => r.text()); chapterText = await fetchApi(json).then(r => r.text());
break; break;
} }
// Last edited in 0.9.0 by Batorian - 19/03/2025
case 'zetrotranslation': {
chapterContent = loadedCheerio('.text-left').html()!;
const titleElement = loadedCheerio('.text-left h2').first();
if (titleElement.length) {
chapterTitle = titleElement.text();
titleElement.remove();
chapterContent = loadedCheerio('.text-left').html()!;
} else if (chapterContent) {
chapterTitle = loadedCheerio('.active').first().text();
}
break;
}
} }
if (!chapterText) { if (!chapterText) {
if (chapterTitle) { if (chapterTitle) {
@@ -833,38 +864,70 @@ class NovelUpdates implements Plugin.PluginBase {
); );
} }
// Detect platforms // Helper to safely check if any element matches a regex or string
let isBlogspot = ['blogspot', 'blogger'].some(keyword => const matches = (selector: string, attr: string | null, regex: RegExp) => {
[ let found = false;
loadedCheerio('meta[name="google-adsense-platform-domain"]').attr( loadedCheerio(selector).each((_, el) => {
'content', const val = attr
), ? loadedCheerio(el).attr(attr)
loadedCheerio('meta[name="generator"]').attr('content'), : loadedCheerio(el).html() || loadedCheerio(el).text();
].some(meta => meta?.toLowerCase().includes(keyword)), if (val && regex.test(val.toLowerCase())) {
); found = true;
return false;
}
});
return found;
};
let isWordPress = ['wordpress', 'site kit by google'].some(keyword => // --- WordPress Detection ---
[ let isWordPress = [
loadedCheerio('#dcl_comments-js-extra').html(), // 1. Meta Generator
loadedCheerio('meta[name="generator"]').attr('content'), matches('meta[name="generator"]', 'content', /wordpress|site kit/i),
loadedCheerio('.powered-by').text(), // 2. Resource Paths (The most reliable way)
loadedCheerio('footer').text(), matches('link, script, img', 'src', /\/wp-content\/|\/wp-includes\//i),
].some(meta => meta?.toLowerCase().includes(keyword)), matches('link', 'href', /\/wp-content\/|\/wp-includes\//i),
); // 3. Header Links (REST API, RSD, etc.)
matches('link[rel="https://api.w.org/"]', 'href', /.*/),
matches('link[rel="EditURI"]', 'href', /xmlrpc\.php/i),
// 4. Common Body Classes
matches('body', 'class', /wp-admin|wp-custom-logo|logged-in/i),
// 5. Scripts containing WP globals
matches('script', null, /wp-embed|wp-emoji|wp-block/i),
].some(Boolean);
// Manually set WordPress flag for known sites // --- Blogspot / Blogger Detection ---
const manualWordPress = ['etherreads', 'greenztl2', 'soafp']; let isBlogspot = [
if (!isWordPress && domainParts.some(wp => manualWordPress.includes(wp))) { // 1. Meta Tags
isWordPress = true; matches('meta[name="generator"]', 'content', /blogger/i),
} matches(
'meta[name="google-adsense-platform-domain"]',
'content',
/blogspot/i,
),
// 2. Feed links
matches(
'link[rel="alternate"]',
'href',
/blogger\.com\/feeds|blogspot\.com\/feeds/i,
),
// 3. Specific Blogger CSS/Template markers
matches(
'link',
'href',
/www\.blogger\.com\/static|www\.blogger\.com\/dyn-css/i,
),
// 4. Blogger Script Widget Manager
matches(
'script',
null,
/_WidgetManager\._Init|_WidgetManager\._RegisterWidget/i,
),
].some(Boolean);
// Handle outlier sites // Handle outlier sites
const outliers = [ const outliers = [
'anotivereads',
'arcanetranslations',
'asuratls', 'asuratls',
'fictionread', 'fictionread',
'helscans',
'hiraethtranslation', 'hiraethtranslation',
'infinitenoveltranslations', 'infinitenoveltranslations',
'machineslicedbread', 'machineslicedbread',
@@ -874,14 +937,13 @@ class NovelUpdates implements Plugin.PluginBase {
'stabbingwithasyringe', 'stabbingwithasyringe',
'tinytranslation', 'tinytranslation',
'vampiramtl', 'vampiramtl',
'zetrotranslation',
]; ];
if (domainParts.some(d => outliers.includes(d))) { if (domainParts.some(d => outliers.includes(d))) {
isWordPress = false; isWordPress = false;
isBlogspot = false; isBlogspot = false;
} }
// Last edited in 0.9.2 - 08/09/2025 // Last edited in 0.9.5 - 26/12/2025
/** /**
* Blogspot sites: * Blogspot sites:
* - ¼-Assed * - ¼-Assed
@@ -892,18 +954,13 @@ class NovelUpdates implements Plugin.PluginBase {
* - Toasteful * - Toasteful
* *
* WordPress sites: * WordPress sites:
* - Anomlaously Creative (Outlier)
* - Arcane Translations (Outlier)
* - Blossom Translation
* - Dumah's Translations * - Dumah's Translations
* - ElloMTL
* - Ether Reads * - Ether Reads
* - Femme Fables * - Femme Fables
* - Gadgetized Panda Translation * - Gadgetized Panda Translation
* - Gem Novels
* - Goblinslate * - Goblinslate
* - Hel Scans (Outlier)
* - Hiraeth Translation (Outlier) * - Hiraeth Translation (Outlier)
* - Infinite Novel Translations (Outlier)
* - ippotranslations * - ippotranslations
* - JATranslations * - JATranslations
* - Light Novels Translations * - Light Novels Translations
@@ -912,98 +969,114 @@ class NovelUpdates implements Plugin.PluginBase {
* - Neosekai Translations * - Neosekai Translations
* - Noice Translations * - Noice Translations
* - Shanghai Fantasy * - Shanghai Fantasy
* - Soafp (Manually added) * - Sleepy Translations
* - Soafp
* - Stabbing with a Syringe (Outlier) * - Stabbing with a Syringe (Outlier)
* - StoneScape * - StoneScape
* - TinyTL (Outlier) * - TinyTL (Outlier)
* - VampiraMTL (Outlier) * - VampiraMTL (Outlier)
* - Wonder Novels * - Wonder Novels
* - Yong Library * - Yong Library
* - Zetro Translation (Outlier) * - Zetro Translation
*/ */
// Fetch chapter content based on detected platform // Define Platform Configurations
const PLATFORM_CONFIG = {
wordpress: {
bloat: [
'.ad',
'.author-avatar',
'.chapter-warning',
'.entry-meta',
'.ezoic-ad',
'.mb-center',
'.modern-footnotes-footnote__note',
'.patreon-widget',
'.post-cats',
'.pre-bar',
'.sharedaddy',
'.sidebar',
'.swg-button-v2-light',
'.wp-block-buttons',
'.wp-dark-mode-switcher',
'.wp-next-post-navi',
'#hpk',
'#jp-post-flair',
'#textbox',
],
title: [
'.entry-title',
'.chapter__title',
'.title-content',
'.wp-block-post-title',
'.title_story',
'#chapter-heading',
'.chapter-title',
'head title',
'h1:first-of-type',
'h2:first-of-type',
'.active',
],
content: [
'.chapter__content',
'.entry-content',
'.text_story',
'.post-content',
'.contenta',
'.single_post',
'.main-content',
'.reader-content',
'#content',
'#the-content',
'article.post',
'.chp_raw',
],
},
blogspot: {
bloat: ['.button-container', '.ChapterNav', '.ch-bottom', '.separator'],
title: ['.entry-title', '.post-title', 'head title'],
content: ['.content-post', '.entry-content', '.post-body'],
},
};
// Extraction Logic
if (!isWordPress && !isBlogspot) { if (!isWordPress && !isBlogspot) {
chapterText = await this.getChapterBody(loadedCheerio, domainParts, url); chapterText = await this.getChapterBody(loadedCheerio, domainParts, url);
} else { } else {
const bloatElements = isBlogspot const config = isWordPress
? ['.button-container', '.ChapterNav', '.ch-bottom', '.separator'] ? PLATFORM_CONFIG.wordpress
: [ : PLATFORM_CONFIG.blogspot;
'.ad',
'.author-avatar',
'.chapter-warning',
'.entry-meta',
'.ezoic-ad',
'.mb-center',
'.modern-footnotes-footnote__note',
'.patreon-widget',
'.post-cats',
'.pre-bar',
'.sharedaddy',
'.sidebar',
'.swg-button-v2-light',
'.wp-block-buttons',
//'.wp-block-columns',
'.wp-dark-mode-switcher',
'.wp-next-post-navi',
'#hpk',
'#jp-post-flair',
'#textbox',
];
bloatElements.forEach(tag => loadedCheerio(tag).remove()); // Remove platform-specific bloat
config.bloat.forEach(tag => loadedCheerio(tag).remove());
// Extract title // Extract Title (Simplified find)
const titleSelectors = isBlogspot let chapterTitle = config.title
? ['.entry-title', '.post-title', 'head title'] .map(sel => loadedCheerio(sel).first().text().trim())
: [ .find(text => text.length > 0);
'.entry-title',
'.chapter__title',
'.title-content',
'.wp-block-post-title',
'.title_story',
'#chapter-heading',
'.chapter-title',
'head title',
'h1:first-of-type',
'h2:first-of-type',
'.active',
];
let chapterTitle = titleSelectors
.map(sel => loadedCheerio(sel).first().text())
.find(text => text);
// Extract subtitle (if any) // Handle Subtitles
const chapterSubtitle = const chapterSubtitle =
loadedCheerio('.cat-series').first().text() || loadedCheerio('.cat-series').first().text() ||
loadedCheerio('h1.leading-none ~ span').first().text(); loadedCheerio('h1.leading-none ~ span').first().text() ||
loadedCheerio('.breadcrumb .active').first().text();
if (chapterSubtitle) chapterTitle = chapterSubtitle; if (chapterSubtitle) chapterTitle = chapterSubtitle;
// Extract content // Extract Content (Scoped search)
const contentSelectors = isBlogspot const chapterContent = config.content
? ['.content-post', '.entry-content', '.post-body'] .map(sel => {
: [ const el = loadedCheerio(sel).first();
'.chapter__content', // Ensure we don't pick up empty containers
'.entry-content', return el.text().trim().length > 50 ? el.html() : null;
'.text_story', })
'.post-content',
'.contenta',
'.single_post',
'.main-content',
'.reader-content',
'#content',
'#the-content',
'article.post',
'.chp_raw',
];
const chapterContent = contentSelectors
.map(sel => loadedCheerio(sel).html()!)
.find(html => html); .find(html => html);
// Construct Final Text
if (chapterTitle && chapterContent) { if (chapterTitle && chapterContent) {
chapterText = `<h2>${chapterTitle}</h2><hr><br>${chapterContent}`; chapterText = `<h2>${chapterTitle}</h2><hr><br>${chapterContent}`;
} else { } else {
chapterText = chapterContent; chapterText = chapterContent || '';
} }
} }
+78 -50
View File
@@ -1,53 +1,3 @@
/**
* Novel Updates -> Arcane Translations
* https://www.novelupdates.com/group/arcane-translations/
* Necessary to display chapterContent correctly
*/
.arcane_box_blue {
text-align: center;
border: 2px solid #00219b;
padding: 20px;
box-shadow: 0 0 10px #00219b;
background-color: black;
position: relative;
z-index: 1;
}
.arcane_box_white {
display: block;
text-align: center;
border: 2px solid white;
padding: 20px;
box-shadow: 0 0 10px white;
background-color: black;
}
.arcane_title_blue {
color: white;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 10px;
text-shadow: 0 0 10px blue;
}
.arcane_text_blue {
color: white;
border-top: 2px solid white;
padding-top: 10px;
text-shadow: 0 0 8px blue;
}
.arcane_text_lightyellow {
color: white;
text-shadow: 0 0 8px lightyellow;
font-family: 'Amita', cursive;
}
.arcane_text_pink {
color: #ff00ff;
}
/** /**
* Novel Updates -> Daoist Quest * Novel Updates -> Daoist Quest
* https://www.novelupdates.com/group/daoist-quest/ * https://www.novelupdates.com/group/daoist-quest/
@@ -436,6 +386,84 @@ div.blue-text {
flex-direction:column flex-direction:column
} }
/**
* Novel Updates -> Novels Hub
* https://www.novelupdates.com/group/novelshub/
* Necessary to display chapterContent correctly
*/
.novels-hub_box_orange {
text-align: center;
border: 2px solid #ff6b00;
padding: 20px;
box-shadow: 0 0 15px rgba(255, 107, 0, 0.4);
position: relative;
margin-bottom: 20px;
}
.novels-hub_box-title_orange {
color: #ff6b00;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 15px;
text-shadow: 0 0 10px rgba(255, 107, 0, 0.6);
}
.novels-hub_box-text_orange {
color: white;
line-height: 1.6;
border-top: 1px solid #ff6b00;
padding-top: 15px;
text-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
}
.novels-hub_box_green {
background: #0d0d0d;
border: 2px solid #00ff88;
padding: 15px 20px;
box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
margin-bottom: 15px;
}
.novels-hub_box-title_green {
color: #00ff88;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
text-shadow: 0 0 8px rgba(0, 255, 136, 0.5);
}
.novels-hub_comment_green {
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
border-left: 4px solid #00ff88;
padding: 15px 20px;
margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.novels-hub_box_blue {
background: rgba(0, 33, 155, 0.15);
border: 1px solid #0066ff;
border-radius: 4px;
padding: 15px 20px;
margin-bottom: 15px;
backdrop-filter: blur(5px);
box-shadow: inset 0 0 10px rgba(0, 102, 255, 0.1);
}
.novels-hub_box-title_blue {
color: #0099ff;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 1px;
}
.novels-hub_box-text_blue {
color: #d0d0d0;
line-height: 1.6;
}
.novels-hub_text_red {
color: #ff6b6b;
}
.novels-hub_text_blue {
color: #4d9fff;
}
.novels-hub_text_purple {
color: #a78bfa;
}
/** /**
* Novel Updates -> Raei Translations * Novel Updates -> Raei Translations
* https://www.novelupdates.com/group/raei-translations/ * https://www.novelupdates.com/group/raei-translations/