Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support locales when resolving templates #303

Open
acarstoiu opened this issue Feb 26, 2025 · 0 comments
Open

Support locales when resolving templates #303

acarstoiu opened this issue Feb 26, 2025 · 0 comments

Comments

@acarstoiu
Copy link

Is your feature request related to a problem?
A fair share of templates produce user-visible contents and thus it makes sense for the template resolution to be aware of locales, including falling back to more general locales when a certain template is not found.

Describe the solution you'd like
I have modified eta's code but I'm not submitting a PR since I worked on the published package code (just the UMD stuff that I use), instead of its TypeScript sources. I've created a patch that will be applied automatically by my package manager (long live pnpm!), but I seem not to be able to attach it here, so I simply paste it below.

diff --git a/dist/eta.umd.js b/dist/eta.umd.js
index 12d4ee8ce978e46720776fdc9e7e11830e25cba5..3b3943c7ea63290fae29f164fcc448316020e6c0 100644
--- a/dist/eta.umd.js
+++ b/dist/eta.umd.js	
@@ -469,32 +469,44 @@ return __eta.res;
   }
 
   /* END TYPES */
+  function isFile(dir, filename) {
+    return fs__namespace.statSync(path__namespace.join(dir, filename), { throwIfNoEntry: false })?.isFile();
+  }
   function handleCache(template, options) {
     const templateStore = options && options.async ? this.templatesAsync : this.templatesSync;
-    if (this.resolvePath && this.readFile && !template.startsWith("@")) {
+    let locale;
+    if (options && options.locale) {
+      if (!(options.locale instanceof Intl.Locale)) {
+        options.locale = new Intl.Locale(options.locale); // implicit validation
+      }
+      locale = options.locale;
+    }
+    if (template === null) {
       const templatePath = options.filepath;
-      const cachedTemplate = templateStore.get(templatePath);
-      if (this.config.cache && cachedTemplate) {
-        return cachedTemplate;
-      } else {
-        const templateString = this.readFile(templatePath);
-        const templateFn = this.compile(templateString, options);
-        if (this.config.cache) templateStore.define(templatePath, templateFn);
-        return templateFn;
+      let actualTemplatePath;
+      if (! (locale?.region && isFile(this.config.views, (actualTemplatePath = path__namespace.join(path__namespace.sep, locale.language, locale.region, templatePath)))
+          || locale && isFile(this.config.views, (actualTemplatePath = path__namespace.join(path__namespace.sep, locale.language, templatePath)))
+          || isFile(this.config.views, (actualTemplatePath = templatePath)))) {
+        throw new EtaFileResolutionError(`Could not find template: ${templatePath}`);
       }
+	  if (this.config.cache) {
+	    const cachedTemplate = templateStore.get(actualTemplatePath);
+	    if (cachedTemplate != null) return cachedTemplate;
+	  }
+      const templateFn = this.compile(this.readFile(actualTemplatePath), options);
+      if (this.config.cache) templateStore.define(actualTemplatePath, templateFn);
+      return templateFn;
     } else {
-      const cachedTemplate = templateStore.get(template);
-      if (cachedTemplate) {
-        return cachedTemplate;
-      } else {
-        throw new EtaNameResolutionError("Failed to get template '" + template + "'");
-      }
+      const cachedTemplate = (locale?.region && templateStore.get(`@${locale.language}-${options.locale.region}${template}`))
+          ?? (locale && templateStore.get(`@${locale.language}${template}`))
+          ?? templateStore.get(template);
+      if (cachedTemplate != null) return cachedTemplate;
+      throw new EtaNameResolutionError("Failed to get template '" + template + "'");
     }
   }
   function render(template,
   // template name or template function
   data, meta) {
-    let templateFn;
     const options = {
       ...meta,
       async: false
@@ -502,18 +514,16 @@ return __eta.res;
     if (typeof template === "string") {
       if (this.resolvePath && this.readFile && !template.startsWith("@")) {
         options.filepath = this.resolvePath(template, options);
+        template = handleCache.call(this, null, options);
+      } else {
+        template = handleCache.call(this, template, options);
       }
-      templateFn = handleCache.call(this, template, options);
-    } else {
-      templateFn = template;
     }
-    const res = templateFn.call(this, data, options);
-    return res;
+    return template.call(this, data, options);
   }
   function renderAsync(template,
   // template name or template function
   data, meta) {
-    let templateFn;
     const options = {
       ...meta,
       async: true
@@ -521,14 +531,13 @@ return __eta.res;
     if (typeof template === "string") {
       if (this.resolvePath && this.readFile && !template.startsWith("@")) {
         options.filepath = this.resolvePath(template, options);
+        template = handleCache.call(this, null, options);
+      } else {
+        template = handleCache.call(this, template, options);
       }
-      templateFn = handleCache.call(this, template, options);
-    } else {
-      templateFn = template;
     }
-    const res = templateFn.call(this, data, options);
     // Return a promise
-    return Promise.resolve(res);
+    return Promise.resolve(template.call(this, data, options));
   }
   function renderString(template, data) {
     const templateFn = this.compile(template, {
@@ -560,8 +569,8 @@ return __eta.res;
       this.templatesSync = new Cacher({});
       this.templatesAsync = new Cacher({});
       // resolvePath takes a relative path from the "views" directory
-      this.resolvePath = null;
-      this.readFile = null;
+//      this.resolvePath = null;
+//      this.readFile = null;
       if (customConfig) {
         this.config = {
           ...defaultConfig,
@@ -592,6 +601,12 @@ return __eta.res;
     loadTemplate(name, template,
     // template string or template function
     options) {
+      if (options && options.locale) {
+        if (!(options.locale instanceof Intl.Locale)) {
+          options.locale = new IntlLocale(locale);
+        }
+        name = `@${options.locale.language}${options.locale.region ? `-${options.locale.region}`: ""}${name}`;
+      }
       if (typeof template === "string") {
         const templates = options && options.async ? this.templatesAsync : this.templatesSync;
         templates.define(name, this.compile(template, options));
@@ -607,65 +622,36 @@ return __eta.res;
 
   /* END TYPES */
   function readFile(path) {
-    let res = "";
-    try {
-      res = fs__namespace.readFileSync(path, "utf8");
-      // eslint-disable-line @typescript-eslint/no-explicit-any
-    } catch (err) {
-      if ((err == null ? void 0 : err.code) === "ENOENT") {
-        throw new EtaFileResolutionError(`Could not find template: ${path}`);
-      } else {
-        throw err;
-      }
-    }
-    return res;
+    return fs__namespace.readFileSync(path__namespace.join(this.config.views, path), "utf8");
   }
   function resolvePath(templatePath, options) {
-    let resolvedFilePath = "";
-    const views = this.config.views;
-    if (!views) {
+    let resolvedFilePath;
+    if (!this.config.views) {
       throw new EtaFileResolutionError("Views directory is not defined");
     }
     const baseFilePath = options && options.filepath;
-    const defaultExtension = this.config.defaultExtension === undefined ? ".eta" : this.config.defaultExtension;
     // how we index cached template paths
-    const cacheIndex = JSON.stringify({
-      filename: baseFilePath,
-      path: templatePath,
-      views: this.config.views
+    const cacheIndex = baseFilePath && this.config.cacheFilepaths && JSON.stringify({
+      base: baseFilePath,
+      path: templatePath
     });
-    templatePath += path__namespace.extname(templatePath) ? "" : defaultExtension;
+    templatePath += path__namespace.extname(templatePath) ? "" : this.config.defaultExtension;
     // if the file was included from another template
     if (baseFilePath) {
       // check the cache
-      if (this.config.cacheFilepaths && this.filepathCache[cacheIndex]) {
+      if (cacheIndex && this.filepathCache[cacheIndex]) {
         return this.filepathCache[cacheIndex];
       }
-      const absolutePathTest = absolutePathRegExp.exec(templatePath);
-      if (absolutePathTest && absolutePathTest.length) {
-        const formattedPath = templatePath.replace(/^\/*|^\\*/, "");
-        resolvedFilePath = path__namespace.join(views, formattedPath);
-      } else {
-        resolvedFilePath = path__namespace.join(path__namespace.dirname(baseFilePath), templatePath);
-      }
+      resolvedFilePath = path__namespace.resolve(path__namespace.dirname(baseFilePath), templatePath);
     } else {
-      resolvedFilePath = path__namespace.join(views, templatePath);
+      resolvedFilePath = path__namespace.resolve(path__namespace.sep, templatePath);
     }
-    if (dirIsChild(views, resolvedFilePath)) {
-      // add resolved path to the cache
-      if (baseFilePath && this.config.cacheFilepaths) {
-        this.filepathCache[cacheIndex] = resolvedFilePath;
-      }
-      return resolvedFilePath;
-    } else {
-      throw new EtaFileResolutionError(`Template '${templatePath}' is not in the views directory`);
+    // add resolved path to the cache
+    if (cacheIndex) {
+      this.filepathCache[cacheIndex] = resolvedFilePath;
     }
+    return resolvedFilePath;
   }
-  function dirIsChild(parent, dir) {
-    const relative = path__namespace.relative(parent, dir);
-    return relative && !relative.startsWith("..") && !path__namespace.isAbsolute(relative);
-  }
-  const absolutePathRegExp = /^\\|^\//;
 
   class Eta extends Eta$1 {
     constructor(...args) {

Summary of my changes:

  • the optional locale is expected to be passed within the options argument of the rendering functions, either as a (string) tag or as an instance of Intl.Locale; only the language and the region matter
  • the code works now under the assumption that the resolved path of a template is further refined when actually retrieving the file's contents (handleCache()), depending on the requested locale, if any; therefore, all resolved paths are absolute paths, as if the views directory was the file system root, and the actual template file must be one of (in this order):
    • <views>/<locale.language>/<locale.region><resolved path>
    • <views>/<locale.language><resolved path>
    • <views><resolved path>
  • I could not resist making optimizations within all functions that I touched
  • I also had to eliminate the direct assignment of resolvePath and readFile functions in EtaCore's constructor as that renders useless its extension in the view of overriding those methods in ECMAScript; the Eta constructor does the same silly thing - it sets those methods on the created instance, undermining the potential changes made in its prototype by subclassing

Describe alternatives you've considered
Locale awareness cannot be implemented by simply overriding Eta.readFile() and Eta.resolvePath(), as the cache operations take place in the inaccesible handleCache() function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant