diff --git a/_config/date.js b/_config/date.js new file mode 100644 index 0000000..531f79c --- /dev/null +++ b/_config/date.js @@ -0,0 +1,31 @@ +const { DateTime } = require("luxon"); + +module.exports = function (eleventyConfig, { TIME_ZONE }) { + eleventyConfig.addDateParsing(function (dateValue) { + // i know this is a deranged solution. sorry LOL + let localDate; + try { + if (dateValue instanceof Date && !isNaN(dateValue)) { + // handle filename dates (ie 2025-10-18-post.md) + localDate = DateTime.fromJSDate(dateValue, { zone: "utc" }) + .setZone(TIME_ZONE) + .startOf("day"); // Set to midnight in America/Santiago + } else if (typeof dateValue === "string" && dateValue) { + // handle string dates (ie from front matter, if used) + localDate = DateTime.fromISO(dateValue, { zone: TIME_ZONE }).startOf("day"); + } else { + // handle invalid input + console.warn(`Invalid date value: ${dateValue} for ${this.page.inputPath}`); + localDate = DateTime.now().setZone(TIME_ZONE).startOf("day"); + } + if (!localDate || localDate.isValid === false) { + throw new Error(`Invalid date value (${dateValue}) for ${this.page.inputPath}: ${localDate?.invalidReason || "Unknown"}`); + } + return localDate.toJSDate(); + } catch (error) { + console.error(`Date parsing error for ${this.page.inputPath}:`, error.message); + // fallback to current date in TIME_ZONE + return DateTime.now().setZone(TIME_ZONE).startOf("day").toJSDate(); + } + }); +}; diff --git a/_config/filters.js b/_config/filters.js new file mode 100644 index 0000000..2f1affe --- /dev/null +++ b/_config/filters.js @@ -0,0 +1,128 @@ +const { DateTime } = require("luxon"); + +module.exports = function (eleventyConfig, { TIME_ZONE, defaultLanguage }) { + // {{ post.date | date("dd/MM/yyyy") }} -> 18/10/2025 + eleventyConfig.addFilter("date", function (dateObj, format = "dd/MM/yyyy") { + let dt = dateObj; + // handle string dates + if (typeof dateObj === "string") { + dt = DateTime.fromISO(dateObj, { zone: TIME_ZONE }).toJSDate(); + } + // handle DateTime objects (from addDateParsing) + if (dateObj instanceof DateTime) { + dt = dateObj.toJSDate(); + } + // check dt as valid Date object + if (!(dt instanceof Date) || isNaN(dt)) { + console.log("Invalid date input:", dateObj); + return ""; + } + // format in TIME_ZONE + const formatted = DateTime.fromJSDate(dt, { zone: TIME_ZONE }) + .toFormat(format); + console.log("Date input:", dt, "Formatted:", formatted, "Timezone:", TIME_ZONE); + return formatted; + }); + + // filters collections by current language + eleventyConfig.addFilter("i18n_filter", function (collection, limit = null) { + const lang = this.page.lang; // access page.lang from context + let filtered = collection.filter(item => item.data.lang === lang); + if (limit !== null) { + filtered = filtered.slice(0, limit); + } + return filtered; + }); + + const LOCALE_URL_DEBUG = process.env.LOCALE_URL_DEBUG === "1" || process.env.LOCALE_URL_DEBUG === "true"; + + // locale_url replacement that uses pre-compile filesystem + eleventyConfig.addNunjucksFilter("locale_url_resolve", function (targetUrl, desiredLocale) { + const ctx = (this && this.ctx) ? this.ctx : {}; + const collections = (ctx.collections) ? ctx.collections : {}; + const all = collections.all || []; + + if (!targetUrl || typeof targetUrl !== "string") return targetUrl; + if (!targetUrl.startsWith("/")) return targetUrl; // external or relative link -> leave as is + + // determine locale to resolve to + const pageLang = desiredLocale || (ctx.page && ctx.page.lang) || ctx.locale || defaultLanguage; + + if (LOCALE_URL_DEBUG) { + console.warn(`[locale_url_resolve] resolving targetUrl="${targetUrl}" desiredLocale="${desiredLocale}" pageLang="${pageLang}"`); + } + + // if requested locale is default, return the raw url + if (pageLang === defaultLanguage) { + if (LOCALE_URL_DEBUG) console.warn(`[locale_url_resolve] requested locale is default (${defaultLanguage}) — returning raw URL "${targetUrl}"`); + return targetUrl; + } + + // normalize targetUrl to ensure trailing slash for comparison + const normUrl = targetUrl.endsWith("/") ? targetUrl : (targetUrl + "/"); + + // try to find the canonical (default language) page corresponding to targetUrl + let canonical = all.find(p => { + return (p.url === normUrl || p.url === targetUrl) && p.data && p.data.lang === defaultLanguage; + }); + + // if not found, try to find any page with that url (maybe targetUrl already localized) + if (!canonical) { + canonical = all.find(p => (p.url === normUrl || p.url === targetUrl)); + } + + if (LOCALE_URL_DEBUG) { + if (canonical) { + const cs = (canonical.page && canonical.page.filePathStem) ? canonical.page.filePathStem : "(no stem)"; + const clang = (canonical.data && canonical.data.lang) ? canonical.data.lang : "(no lang)"; + console.warn(`[locale_url_resolve] canonical found: url="${canonical.url}" filePathStem="${cs}" lang="${clang}"`); + } else { + console.warn(`[locale_url_resolve] canonical NOT found for targetUrl="${targetUrl}". Will fallback to prefixed URL.`); + } + } + + // if cannot find canonical page, fall back to a prefixed URL (best effort) + if (!canonical) { + const fallback = (`/${pageLang}${targetUrl}`).replace(/\/{2,}/g, "/"); + if (LOCALE_URL_DEBUG) console.warn(`[locale_url_resolve] fallback -> "${fallback}"`); + return fallback; + } + + // determine canonical filePathStem (the source input path without extension) + const canonicalLang = canonical.data && canonical.data.lang ? canonical.data.lang : defaultLanguage; + const canonicalStem = (canonical.page && canonical.page.filePathStem) ? canonical.page.filePathStem : ""; + + // remove the canonical lang prefix from the stem to create a "key" to match across locales. + // ie "/en/about" -> "/about", "/es/about" -> "/about" + const key = canonicalStem.replace(new RegExp(`^/${canonicalLang}`), "").replace(/^\/+/, ""); + + if (LOCALE_URL_DEBUG) { + console.warn(`[locale_url_resolve] canonicalLang="${canonicalLang}" canonicalStem="${canonicalStem}" key="${key}"`); + } + + // find the localized page whose filePathStem ends with that key and whose lang matches pageLang. + const localized = all.find(p => { + const pLang = p.data && p.data.lang; + const pStem = (p.page && p.page.filePathStem) ? p.page.filePathStem : ""; + // be defensive: ensure pLang exists + if (!pLang) return false; + return pLang === pageLang && pStem.endsWith(key); + }); + + if (localized && localized.url) { + if (LOCALE_URL_DEBUG) { + const ls = (localized.page && localized.page.filePathStem) ? localized.page.filePathStem : "(no stem)"; + console.warn(`[locale_url_resolve] localized found: url="${localized.url}" filePathStem="${ls}" lang="${localized.data.lang}"`); + } + return localized.url; + } + + // no localized page found — fall back to prefixed path + const fallback2 = (`/${pageLang}${targetUrl}`).replace(/\/{2,}/g, "/"); + if (LOCALE_URL_DEBUG) console.warn(`[locale_url_resolve] localized NOT found for key="${key}" — fallback -> "${fallback2}"`); + return fallback2; + }); + + // turn on disabled nunjucks filter + eleventyConfig.addNunjucksFilter("values", obj => Object.values(obj)); +}; diff --git a/_config/plugins.js b/_config/plugins.js new file mode 100644 index 0000000..53958bc --- /dev/null +++ b/_config/plugins.js @@ -0,0 +1,31 @@ +const { I18nPlugin } = require("@11ty/eleventy"); +const i18n = require("eleventy-plugin-i18n"); +const eleventySass = require("eleventy-sass"); +const toml = require("@iarna/toml"); +const fs = require("fs"); +const path = require("path"); + +module.exports = function (eleventyConfig, { defaultLanguage }) { + // load translations + const translationsToml = fs.readFileSync( + path.join(__dirname, "..", "_data", "locale.toml"), + "utf-8" + ); + const translations = toml.parse(translationsToml); + + // plugins + eleventyConfig.addPlugin(eleventySass); + eleventyConfig.addPlugin(I18nPlugin, { + defaultLanguage, + errorMode: "allow-fallback", + }); + eleventyConfig.addPlugin(i18n, { + translations, + fallbackLocales: { + "*": "en", + } + }); + + // data extensions + eleventyConfig.addDataExtension("toml", (contents) => toml.parse(contents)); +}; \ No newline at end of file diff --git a/_config/utils.js b/_config/utils.js new file mode 100644 index 0000000..387ae1e --- /dev/null +++ b/_config/utils.js @@ -0,0 +1,7 @@ +const TIME_ZONE = "America/Santiago"; +const defaultLanguage = "en"; + +module.exports = { + TIME_ZONE, + defaultLanguage +}; \ No newline at end of file diff --git a/eleventy.config.js b/eleventy.config.js index cbc0c92..3ab00b8 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -1,44 +1,9 @@ -const { I18nPlugin } = require("@11ty/eleventy"); -const { DateTime } = require("luxon"); - -const i18n = require("eleventy-plugin-i18n"); -const eleventySass = require("eleventy-sass"); -const toml = require("@iarna/toml"); - -const fs = require("fs"); -const path = require("path"); - -const TIME_ZONE = "America/Santiago"; +const { TIME_ZONE, defaultLanguage } = require("./_config/utils"); +const addDateParsing = require("./_config/date"); +const addFilters = require("./_config/filters"); +const addPlugins = require("./_config/plugins"); module.exports = function (eleventyConfig) { - eleventyConfig.addDateParsing(function (dateValue) { - // i know this is a deranged solution. sorry LOL - let localDate; - try { - if (dateValue instanceof Date && !isNaN(dateValue)) { - // handle filename dates (ie 2025-10-18-post.md) - localDate = DateTime.fromJSDate(dateValue, { zone: "utc" }) - .setZone(TIME_ZONE) - .startOf("day"); // Set to midnight in America/Santiago - } else if (typeof dateValue === "string" && dateValue) { - // handle string dates (ie from front matter, if used) - localDate = DateTime.fromISO(dateValue, { zone: TIME_ZONE }).startOf("day"); - } else { - // handle invalid input - console.warn(`Invalid date value: ${dateValue} for ${this.page.inputPath}`); - localDate = DateTime.now().setZone(TIME_ZONE).startOf("day"); - } - if (!localDate || localDate.isValid === false) { - throw new Error(`Invalid date value (${dateValue}) for ${this.page.inputPath}: ${localDate?.invalidReason || "Unknown"}`); - } - return localDate.toJSDate(); - } catch (error) { - console.error(`Date parsing error for ${this.page.inputPath}:`, error.message); - // fallback to current date in TIME_ZONE - return DateTime.now().setZone(TIME_ZONE).startOf("day").toJSDate(); - } - }); - eleventyConfig.setLayoutsDirectory("_layouts"); eleventyConfig.addPassthroughCopy("img"); @@ -48,155 +13,18 @@ module.exports = function (eleventyConfig) { eleventyConfig.addPassthroughCopy("robots.txt"); eleventyConfig.addPassthroughCopy("roms"); - const defaultLanguage = "en"; - // expose as global site data for templates + // add global site data for templates + // TODO: move _config/utils.js and global site data to _data/meta.toml eleventyConfig.addGlobalData("site", { defaultLocale: defaultLanguage }); - eleventyConfig.addNunjucksFilter("values", obj => Object.values(obj)); - - eleventyConfig.addFilter("i18n_filter", function (collection, limit = null) { - const lang = this.page.lang; // access page.lang from context - let filtered = collection.filter(item => item.data.lang === lang); - if (limit !== null) { - filtered = filtered.slice(0, limit); - } - return filtered; - }); - - eleventyConfig.addFilter("date", function (dateObj, format = "dd/MM/yyyy") { - let dt = dateObj; - // handle string dates - if (typeof dateObj === "string") { - dt = DateTime.fromISO(dateObj, { zone: TIME_ZONE }).toJSDate(); - } - // handle DateTime objects (from addDateParsing) - if (dateObj instanceof DateTime) { - dt = dateObj.toJSDate(); - } - // check dt as valid Date object - if (!(dt instanceof Date) || isNaN(dt)) { - console.log("Invalid date input:", dateObj); - return ""; - } - // format in TIME_ZONE - const formatted = DateTime.fromJSDate(dt, { zone: TIME_ZONE }) - .toFormat(format); - console.log("Date input:", dt, "Formatted:", formatted, "Timezone:", TIME_ZONE); - return formatted; - }); - - eleventyConfig.addPlugin(eleventySass); - eleventyConfig.addPlugin(I18nPlugin, { - defaultLanguage: defaultLanguage, - errorMode: "allow-fallback", // /en/ -> / - }); - eleventyConfig.addDataExtension("toml", (contents) => toml.parse(contents)); - - const translationsToml = fs.readFileSync( - path.join(__dirname, "_data", "locale.toml"), - "utf-8" - ); - const translations = toml.parse(translationsToml); - - const LOCALE_URL_DEBUG = process.env.LOCALE_URL_DEBUG === "1" || process.env.LOCALE_URL_DEBUG === "true"; - - eleventyConfig.addNunjucksFilter("locale_url_resolve", function (targetUrl, desiredLocale) { - const ctx = (this && this.ctx) ? this.ctx : {}; - const collections = (ctx.collections) ? ctx.collections : {}; - const all = collections.all || []; - - if (!targetUrl || typeof targetUrl !== "string") return targetUrl; - if (!targetUrl.startsWith("/")) return targetUrl; // external or relative link -> leave as is - - // determine locale to resolve to - const pageLang = desiredLocale || (ctx.page && ctx.page.lang) || ctx.locale || defaultLanguage; - - if (LOCALE_URL_DEBUG) { - console.warn(`[locale_url_resolve] resolving targetUrl="${targetUrl}" desiredLocale="${desiredLocale}" pageLang="${pageLang}"`); - } - - // if requested locale is default, return the raw url (your default publishes to root) - if (pageLang === defaultLanguage) { - if (LOCALE_URL_DEBUG) console.warn(`[locale_url_resolve] requested locale is default (${defaultLanguage}) — returning raw URL "${targetUrl}"`); - return targetUrl; - } - - // normalize targetUrl to ensure trailing slash for comparison - const normUrl = targetUrl.endsWith("/") ? targetUrl : (targetUrl + "/"); - - // try to find the canonical (default language) page corresponding to targetUrl - let canonical = all.find(p => { - return (p.url === normUrl || p.url === targetUrl) && p.data && p.data.lang === defaultLanguage; - }); - - // if not found, try to find any page with that url (maybe targetUrl already localized) - if (!canonical) { - canonical = all.find(p => (p.url === normUrl || p.url === targetUrl)); - } - - if (LOCALE_URL_DEBUG) { - if (canonical) { - const cs = (canonical.page && canonical.page.filePathStem) ? canonical.page.filePathStem : "(no stem)"; - const clang = (canonical.data && canonical.data.lang) ? canonical.data.lang : "(no lang)"; - console.warn(`[locale_url_resolve] canonical found: url="${canonical.url}" filePathStem="${cs}" lang="${clang}"`); - } else { - console.warn(`[locale_url_resolve] canonical NOT found for targetUrl="${targetUrl}". Will fallback to prefixed URL.`); - } - } - - // if cannot find canonical page, fall back to a prefixed URL (best effort) - if (!canonical) { - const fallback = (`/${pageLang}${targetUrl}`).replace(/\/{2,}/g, "/"); - if (LOCALE_URL_DEBUG) console.warn(`[locale_url_resolve] fallback -> "${fallback}"`); - return fallback; - } - - // determine canonical filePathStem (the source input path without extension) - const canonicalLang = canonical.data && canonical.data.lang ? canonical.data.lang : defaultLanguage; - const canonicalStem = (canonical.page && canonical.page.filePathStem) ? canonical.page.filePathStem : ""; - - // remove the canonical lang prefix from the stem to create a "key" we can match across locales. - // e.g. "/en/about" -> "/about", "/es/about" -> "/about" - const key = canonicalStem.replace(new RegExp(`^/${canonicalLang}`), "").replace(/^\/+/, ""); - - if (LOCALE_URL_DEBUG) { - console.warn(`[locale_url_resolve] canonicalLang="${canonicalLang}" canonicalStem="${canonicalStem}" key="${key}"`); - } - - // find the localized page whose filePathStem ends with that key and whose lang matches pageLang. - const localized = all.find(p => { - const pLang = p.data && p.data.lang; - const pStem = (p.page && p.page.filePathStem) ? p.page.filePathStem : ""; - // be defensive: ensure pLang exists - if (!pLang) return false; - return pLang === pageLang && pStem.endsWith(key); - }); - - if (localized && localized.url) { - if (LOCALE_URL_DEBUG) { - const ls = (localized.page && localized.page.filePathStem) ? localized.page.filePathStem : "(no stem)"; - console.warn(`[locale_url_resolve] localized found: url="${localized.url}" filePathStem="${ls}" lang="${localized.data.lang}"`); - } - return localized.url; - } - - // no localized page found — fall back to prefixed path - const fallback2 = (`/${pageLang}${targetUrl}`).replace(/\/{2,}/g, "/"); - if (LOCALE_URL_DEBUG) console.warn(`[locale_url_resolve] localized NOT found for key="${key}" — fallback -> "${fallback2}"`); - return fallback2; - }); - - eleventyConfig.addPlugin(i18n, { - translations, - fallbackLocales: { - "*": "en", - } - }); + addDateParsing(eleventyConfig, { TIME_ZONE }); + addFilters(eleventyConfig, { TIME_ZONE, defaultLanguage }); + addPlugins(eleventyConfig, { defaultLanguage }); return { markdownTemplateEngine: "njk", htmlTemplateEngine: "njk" } -}; \ No newline at end of file +};