Skip to content

Commit 0d9c5e8

Browse files
committed
add docx
1 parent 4267e05 commit 0d9c5e8

18 files changed

Lines changed: 2889 additions & 19 deletions

File tree

dist/index.js

Lines changed: 25 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/server/public/app.js

Lines changed: 671 additions & 1 deletion
Large diffs are not rendered by default.

dist/server/public/i18n.js

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,169 @@
1-
class I18n{constructor(){this.currentLanguage="en",this.translations={},this.fallbackLanguage="en"}async loadLanguage(t){try{const e=await fetch(`/locales/${t}.json`);if(!e.ok)throw new Error(`Failed to load language file: ${t}`);return this.translations[t]=await e.json(),!0}catch(e){return console.error(`Error loading language ${t}:`,e),!1}}async setLanguage(t){if(!this.translations[t]){await this.loadLanguage(t)||t===this.fallbackLanguage||(await this.loadLanguage(this.fallbackLanguage),t=this.fallbackLanguage)}this.currentLanguage=t,localStorage.setItem("language",t),this.updatePageTranslations(),this.updateLanguageSelector(),this.updateDynamicContent()}t(t,e=""){return this.translations[this.currentLanguage]?.[t]||this.translations[this.fallbackLanguage]?.[t]||e||t}updatePageTranslations(){document.querySelectorAll("[data-i18n]").forEach((t=>{const e=t.getAttribute("data-i18n"),a=this.t(e);a.includes("<")&&a.includes(">")?t.innerHTML=a:t.textContent=a})),document.querySelectorAll("[data-i18n-placeholder]").forEach((t=>{const e=t.getAttribute("data-i18n-placeholder");t.placeholder=this.t(e)})),document.querySelectorAll("[data-i18n-title]").forEach((t=>{const e=t.getAttribute("data-i18n-title");t.title=this.t(e)})),document.querySelectorAll("[data-i18n-html]").forEach((t=>{const e=t.getAttribute("data-i18n-html");t.innerHTML=this.t(e)})),document.querySelectorAll("[data-i18n-description]").forEach((t=>{const e=t.getAttribute("data-i18n-description"),a=this.t(e);t.setAttribute("data-description",a)}))}updateDynamicContent(){const t=document.querySelector('input[name="preset"]:checked');if(t&&t.dataset.descriptionKey){const e=document.getElementById("preset-description");if(e&&"none"!==e.style.display){const a=e.querySelector("p");if(a){const e=this.t(t.dataset.descriptionKey);a.innerHTML=e}}}const e=document.querySelector('input[name="format"]:checked');if(e&&e.dataset.description){const t=document.getElementById("format-description");if(t&&"none"!==t.style.display){const a=t.querySelector("p");if(a){const t=e.dataset.description;a.innerHTML=t}}}const a=document.querySelectorAll(".remove-file");if(a.length>0){const t=this.t("files.remove");a.forEach((e=>{e.title=t}))}}updateLanguageSelector(){const t=document.getElementById("language-selector");t&&(t.value=this.currentLanguage)}async init(){let t=localStorage.getItem("language")||navigator.language.split("-")[0]||"en";["en","de"].includes(t)||(t="en"),await this.loadLanguage(this.fallbackLanguage),t!==this.fallbackLanguage&&await this.loadLanguage(t),this.currentLanguage=t,this.updatePageTranslations(),this.updateLanguageSelector();const e=document.getElementById("language-selector");e&&e.addEventListener("change",(t=>{this.setLanguage(t.target.value)}))}}const i18n=new I18n;"loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>i18n.init())):i18n.init(),window.i18n=i18n;
1+
/**
2+
* i18n.js - Internationalization system
3+
* Supports JSON translation files with dot notation keys
4+
*/
5+
6+
class I18n {
7+
constructor() {
8+
this.currentLanguage = 'en'
9+
this.translations = {}
10+
this.fallbackLanguage = 'en'
11+
}
12+
13+
async loadLanguage(lang) {
14+
try {
15+
const response = await fetch(`/locales/${lang}.json`)
16+
if (!response.ok) {
17+
throw new Error(`Failed to load language file: ${lang}`)
18+
}
19+
this.translations[lang] = await response.json()
20+
return true
21+
} catch (error) {
22+
console.error(`Error loading language ${lang}:`, error)
23+
return false
24+
}
25+
}
26+
27+
async setLanguage(lang) {
28+
// Load language if not already loaded
29+
if (!this.translations[lang]) {
30+
const success = await this.loadLanguage(lang)
31+
if (!success && lang !== this.fallbackLanguage) {
32+
// Try fallback
33+
await this.loadLanguage(this.fallbackLanguage)
34+
lang = this.fallbackLanguage
35+
}
36+
}
37+
38+
this.currentLanguage = lang
39+
localStorage.setItem('language', lang)
40+
this.updatePageTranslations()
41+
this.updateLanguageSelector()
42+
this.updateDynamicContent()
43+
}
44+
45+
t(key, fallback = '') {
46+
const translation = this.translations[this.currentLanguage]?.[key] ||
47+
this.translations[this.fallbackLanguage]?.[key] ||
48+
fallback ||
49+
key
50+
return translation
51+
}
52+
53+
// Update all elements with data-i18n attribute
54+
updatePageTranslations() {
55+
document.querySelectorAll('[data-i18n]').forEach(element => {
56+
const key = element.getAttribute('data-i18n')
57+
const translation = this.t(key)
58+
59+
if (translation.includes('<') && translation.includes('>')) {
60+
element.innerHTML = translation
61+
} else {
62+
element.textContent = translation
63+
}
64+
})
65+
66+
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
67+
const key = element.getAttribute('data-i18n-placeholder')
68+
element.placeholder = this.t(key)
69+
})
70+
71+
document.querySelectorAll('[data-i18n-title]').forEach(element => {
72+
const key = element.getAttribute('data-i18n-title')
73+
element.title = this.t(key)
74+
})
75+
76+
document.querySelectorAll('[data-i18n-html]').forEach(element => {
77+
const key = element.getAttribute('data-i18n-html')
78+
element.innerHTML = this.t(key)
79+
})
80+
81+
document.querySelectorAll('[data-i18n-description]').forEach(element => {
82+
const key = element.getAttribute('data-i18n-description')
83+
const translation = this.t(key)
84+
element.setAttribute('data-description', translation)
85+
})
86+
}
87+
88+
updateDynamicContent() {
89+
// Update preset description if one is currently selected
90+
const checkedPreset = document.querySelector('input[name="preset"]:checked')
91+
if (checkedPreset && checkedPreset.dataset.descriptionKey) {
92+
const descriptionBox = document.getElementById('preset-description')
93+
if (descriptionBox && descriptionBox.style.display !== 'none') {
94+
const descriptionText = descriptionBox.querySelector('p')
95+
if (descriptionText) {
96+
const description = this.t(checkedPreset.dataset.descriptionKey)
97+
descriptionText.innerHTML = description
98+
}
99+
}
100+
}
101+
102+
// Update format description if one is currently selected
103+
const checkedFormat = document.querySelector('input[name="format"]:checked')
104+
if (checkedFormat && checkedFormat.dataset.description) {
105+
const descriptionBox = document.getElementById('format-description')
106+
if (descriptionBox && descriptionBox.style.display !== 'none') {
107+
const descriptionText = descriptionBox.querySelector('p')
108+
if (descriptionText) {
109+
const description = checkedFormat.dataset.description
110+
descriptionText.innerHTML = description
111+
}
112+
}
113+
}
114+
115+
// Update file list remove button titles
116+
const removeButtons = document.querySelectorAll('.remove-file')
117+
if (removeButtons.length > 0) {
118+
const removeTitle = this.t('files.remove')
119+
removeButtons.forEach(btn => {
120+
btn.title = removeTitle
121+
})
122+
}
123+
}
124+
125+
updateLanguageSelector() {
126+
const selector = document.getElementById('language-selector')
127+
if (selector) {
128+
selector.value = this.currentLanguage
129+
}
130+
}
131+
132+
async init() {
133+
const savedLang = localStorage.getItem('language')
134+
135+
let initialLang = savedLang || navigator.language.split('-')[0] || 'en'
136+
137+
const supportedLanguages = ['en', 'de']
138+
if (!supportedLanguages.includes(initialLang)) {
139+
initialLang = 'en'
140+
}
141+
142+
await this.loadLanguage(this.fallbackLanguage)
143+
144+
if (initialLang !== this.fallbackLanguage) {
145+
await this.loadLanguage(initialLang)
146+
}
147+
148+
this.currentLanguage = initialLang
149+
this.updatePageTranslations()
150+
this.updateLanguageSelector()
151+
152+
const selector = document.getElementById('language-selector')
153+
if (selector) {
154+
selector.addEventListener('change', (e) => {
155+
this.setLanguage(e.target.value)
156+
})
157+
}
158+
}
159+
}
160+
161+
const i18n = new I18n()
162+
163+
if (document.readyState === 'loading') {
164+
document.addEventListener('DOMContentLoaded', () => i18n.init())
165+
} else {
166+
i18n.init()
167+
}
168+
169+
window.i18n = i18n

dist/server/public/index.html

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,21 @@ <h3 data-i18n="formats.epub.title">EPUB</h3>
309309
</div>
310310
</label>
311311

312+
<label class="preset-tile">
313+
<input
314+
type="radio"
315+
name="format"
316+
value="docx"
317+
data-i18n-description="formats.docx.description"
318+
data-description="Microsoft Word document format for professional documents. Compatible with Word 2007+, LibreOffice Writer, and Google Docs. <a href='https://en.wikipedia.org/wiki/Office_Open_XML' target='_blank'>Learn more</a>"
319+
/>
320+
<div class="preset-content">
321+
<div class="format-icon">📝</div>
322+
<h3 data-i18n="formats.docx.title">DOCX</h3>
323+
<p data-i18n="formats.docx.subtitle">Word Document</p>
324+
</div>
325+
</label>
326+
312327
<label class="preset-tile">
313328
<input
314329
type="radio"
@@ -1109,6 +1124,166 @@ <h3 class="settings-heading" data-i18n="settings.epub.title">
11091124
</div>
11101125
</div>
11111126

1127+
<!-- DOCX Settings -->
1128+
<div class="settings-group" data-formats="docx">
1129+
<h3 class="settings-heading" data-i18n="settings.docx.title">
1130+
DOCX Settings
1131+
</h3>
1132+
1133+
<div class="form-group">
1134+
<label for="docxTitle" data-i18n="settings.docx.docTitle"
1135+
>Document Title</label
1136+
>
1137+
<input
1138+
type="text"
1139+
id="docxTitle"
1140+
name="option_docx-title"
1141+
data-i18n-placeholder="settings.docx.docTitlePlaceholder"
1142+
placeholder="My Course"
1143+
/>
1144+
<span class="hint" data-i18n="settings.docx.docTitleHint"
1145+
>Title of the Word document</span
1146+
>
1147+
</div>
1148+
1149+
<div class="form-group">
1150+
<label for="docxAuthor" data-i18n="settings.docx.author"
1151+
>Author</label
1152+
>
1153+
<input
1154+
type="text"
1155+
id="docxAuthor"
1156+
name="option_docx-author"
1157+
data-i18n-placeholder="settings.docx.authorPlaceholder"
1158+
placeholder="John Doe"
1159+
/>
1160+
<span class="hint" data-i18n="settings.docx.authorHint"
1161+
>Author of the document</span
1162+
>
1163+
</div>
1164+
1165+
<div class="form-group">
1166+
<label for="docxOrientation" data-i18n="settings.docx.orientation"
1167+
>Orientation</label
1168+
>
1169+
<select id="docxOrientation" name="option_docx-orientation">
1170+
<option value="portrait" selected data-i18n="settings.docx.orientationPortrait">Portrait</option>
1171+
<option value="landscape" data-i18n="settings.docx.orientationLandscape">Landscape</option>
1172+
</select>
1173+
<span class="hint" data-i18n="settings.docx.orientationHint"
1174+
>Page orientation</span
1175+
>
1176+
</div>
1177+
1178+
<div class="form-group">
1179+
<label for="docxFont" data-i18n="settings.docx.font"
1180+
>Font</label
1181+
>
1182+
<input
1183+
type="text"
1184+
id="docxFont"
1185+
name="option_docx-font"
1186+
placeholder="Arial"
1187+
data-i18n-placeholder="settings.docx.fontPlaceholder"
1188+
/>
1189+
<span class="hint" data-i18n="settings.docx.fontHint"
1190+
>Font name (default: Arial)</span
1191+
>
1192+
</div>
1193+
1194+
<div class="form-group">
1195+
<label for="docxFontSize" data-i18n="settings.docx.fontSize"
1196+
>Font Size</label
1197+
>
1198+
<input
1199+
type="number"
1200+
id="docxFontSize"
1201+
name="option_docx-font-size"
1202+
placeholder="22"
1203+
min="10"
1204+
max="100"
1205+
/>
1206+
<span class="hint" data-i18n="settings.docx.fontSizeHint"
1207+
>Font size in half-points (22 = 11pt)</span
1208+
>
1209+
</div>
1210+
1211+
<div class="form-group">
1212+
<label for="docxTheme" data-i18n="settings.docx.theme"
1213+
>LiaScript Theme</label
1214+
>
1215+
<select id="docxTheme" name="option_docx-theme">
1216+
<option value="" selected data-i18n="settings.docx.themeDefault">Default</option>
1217+
<option value="turquoise" data-i18n="settings.docx.themeTurquoise">Turquoise</option>
1218+
<option value="blue" data-i18n="settings.docx.themeBlue">Blue</option>
1219+
<option value="red" data-i18n="settings.docx.themeRed">Red</option>
1220+
<option value="yellow" data-i18n="settings.docx.themeYellow">Yellow</option>
1221+
</select>
1222+
<span class="hint" data-i18n="settings.docx.themeHint"
1223+
>Color scheme for the document</span
1224+
>
1225+
</div>
1226+
1227+
<div class="form-group">
1228+
<label>
1229+
<input
1230+
type="checkbox"
1231+
id="docxHeader"
1232+
name="option_docx-header"
1233+
/>
1234+
<span data-i18n="settings.docx.header">Enable Header</span>
1235+
</label>
1236+
<span class="hint" data-i18n="settings.docx.headerHint"
1237+
>Add a header to every page</span
1238+
>
1239+
</div>
1240+
1241+
<div class="form-group">
1242+
<label>
1243+
<input
1244+
type="checkbox"
1245+
id="docxFooter"
1246+
name="option_docx-footer"
1247+
/>
1248+
<span data-i18n="settings.docx.footer">Enable Footer</span>
1249+
</label>
1250+
<span class="hint" data-i18n="settings.docx.footerHint"
1251+
>Add a footer to every page</span
1252+
>
1253+
</div>
1254+
1255+
<div class="form-group">
1256+
<label>
1257+
<input
1258+
type="checkbox"
1259+
id="docxPageNumber"
1260+
name="option_docx-page-number"
1261+
/>
1262+
<span data-i18n="settings.docx.pageNumber">Page Numbers</span>
1263+
</label>
1264+
<span class="hint" data-i18n="settings.docx.pageNumberHint"
1265+
>Add page numbers to the footer</span
1266+
>
1267+
</div>
1268+
1269+
<div class="form-group">
1270+
<label for="docxTimeout" data-i18n="settings.docx.timeout"
1271+
>Timeout (ms)</label
1272+
>
1273+
<input
1274+
type="number"
1275+
id="docxTimeout"
1276+
name="option_docx-timeout"
1277+
placeholder="15000"
1278+
min="1000"
1279+
max="120000"
1280+
/>
1281+
<span class="hint" data-i18n="settings.docx.timeoutHint"
1282+
>Wait time for complete loading (15000 = 15 seconds)</span
1283+
>
1284+
</div>
1285+
</div>
1286+
11121287
<!-- JSON Settings -->
11131288
<div class="settings-group" data-formats="json">
11141289
<h3 class="settings-heading" data-i18n="settings.json.title">

0 commit comments

Comments
 (0)