Informatik Handwerk
Peter Fargaš
Programmer :: Prototyping, Research
PHP | JavaScript | Java
Informatik Handwerk
Peter Fargaš
Programmer :: Prototyping, Research
PHP | JavaScript | Java
Informatik Handwerk
Peter Fargaš | Programmer :: Prototyping, Research | PHP,JavaScript,Java
Release date: October 2016
Link to authoritative version https://knowledge-transfer.informatik-handwerk.de
/article/tool/basicTranslationForSquarespace.php

Basic Translation for Squarespace

This script translates what is common to all templates: the mobile bar, contact formular and commenting. You can try it directly out by downloading from here.

It worked great in 2016, probably still works fine, and is of much higher quality than the snippets you will find elsewhere. It is however short of being perfect and might promise just a bit too much. Below, you will find my offer for a professional solution if it should not be enough.

Professional solution means

  • It is cross browser compatible and has fallback even for very old browsers.
  • It behaves well in Squarespace edit mode - it executes only on live site.
  • It never collides with plugins and code of others, everything is well prefixed.
  • The customized sourecodes are hosted. The snippet is compressed and the included code on your side is just a coupleof lines instead of the whole machinery.

Also, if you need more complete translation, or need template specific adjustments, just contact me.

Open-Source solution

The basic usage is very simple. Just copy and paste the snippet under SETTINGS>ADVANCED>CODE INJECTION into the HEADER field. I decided to use jQuery instead of the built-in YUI, so if you already have it in the HEADER field, you should leave this first line out. The provided example is in German. Just replace those translation strings with the language of your preference and that's it.

I will thus not further waste time with the superficial and obvious but if you are interested, I will later focus on code-quality and get into theoretical aspects unproportional to the code snippet itself.

<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">
  basicTranslation_dictionary = {
    "mobileBar": {
        "Email":     "Email", 
        "Call":      "Anruf", 
        "Map":       "Karte", 
        "Hours":     "Zeiten"
      }, 
    "contactFormLabel": {
        "First Name":     "Vorname", 
        "Last Name":      "Nachname"
      }, 
    "contactFormError": {
        "is required.": "ist erforderlich.", 
        "is not valid. Email addresses should follow the format user@domain.com.": "folgt die Konvention benutzer@domain.com.", 
        "Your form has encountered a problem.": "Oops, da ging etwas schief.",
        "Please scroll down to review.": "", 
        "Please scroll up to review.": ""
      }, 
    "commentingSectionHeader": {
        "Comments":                 "Kommentare", 
        "Newest First":             "Neuste zuerst", 
        "Oldest First":             "Älteste zuerst", 
        "Most Liked":               "Beliebteste zuerst", 
        "Least Liked":              "Unbeliebteste zuerst", 
        "Subscribe via e-mail":     "Per E-Mail Abonieren"
      }, 
    "commentingEntryHeader": {
        "Pending":                 "In Prozess", 
        "Awaiting Moderation":     "Wartet auf Moderator", 
        "Not Logged In":           "Nicht eingelogged"
      }, 
    "commentingEntryButtons": {
        "Preview":           "Vorschau", 
        "Edit":              "Editieren", 
        "Post Comment":      "Veröffentlichen", 
        "Post Reply":        "Antworten"
      }, 
    "commentingTime": {
        "Just now":         "Jetzt", 
        "An":               "", 
        "A":                "", 
        "minute ago":       "Minute alt", 
        "minutes ago":      "Minuten alt", 
        "hour ago":         "Stunde alt", 
        "hours ago":        "Stunden alt", 
        "day ago":          "Tag alt", 
        "days ago":         "Tage alt"
    }
  };

  /************************************************************************************/
  basicTranslation_selectors = {
    "mobileBar":                   ".sqs-mobile-info-bar-trigger-label", //direct, assign
    "contactFormLabel":            "form label.caption", //filter, assign
    "contactFormError":            "form div.field-error", //direct, replace
    "commentingSectionHeader":     "h3.comment-count span, div.comment-controls span, div.comment-controls option", //direct, assign
    "commentingEntryHeader":       "div.comment-header span", //direct, replace
    "commentingEntryButtons":      "div.comment-btn-wrapper>span", //direct, assign
    "commentingTime":              "div.comment-header span.timesince" //direct, replace
  };

  /************************************************************************************/
  /************************************************************************************/
  /************************************************************************************/
  jQuery(document).ready( function() {
    
    var mutationObserver = new MutationObserver(basicTranslation_mutationObserver);
    mutationObserver.observe(document, { subtree: true, childList: true, characterData: true });
    
    basicTranslation_naiveReplace(jQuery(basicTranslation_selectors["mobileBar"]), "mobileBar", basicTranslation_isTextNode);
    basicTranslation_naiveReplace(jQuery(basicTranslation_selectors["contactFormLabel"]), "contactFormLabel", basicTranslation_isTextNode);
  }); //onStartup

  /************************************************************************************/
  basicTranslation_isTextNode = function() {
    return this.nodeType === 3;
  }; //routine
  basicTranslation_isAnyNode = function() {
    return true;
  }; //routine

  /************************************************************************************/
  basicTranslation_mutationObserver = function(mutations, observer) {
    
    for (var i=0; i<mutations.length; i++) {
      var mutationRecord = mutations[i];
      switch (mutationRecord.type) {
        case "childList":
          if (!("addedNodes" in mutationRecord) || mutationRecord.addedNodes.length===0) {
            break;
          } //if
          
          var addedNodes = jQuery(mutationRecord.addedNodes);
          
          basicTranslation_naiveReplace(addedNodes, "contactFormLabel", basicTranslation_isTextNode);
          basicTranslation_naiveReplace(addedNodes, "contactFormError", basicTranslation_isTextNode);
          basicTranslation_naiveReplace(addedNodes, "commentingSectionHeader", basicTranslation_isTextNode);
          basicTranslation_naiveReplace(addedNodes, "commentingEntryHeader", basicTranslation_isTextNode);
          basicTranslation_naiveReplace(addedNodes, "commentingEntryButtons", basicTranslation_isTextNode);
          basicTranslation_naiveReplace(addedNodes, "commentingTime", basicTranslation_isTextNode);
          break;
          
        case "characterData": 
          
          switch (mutationRecord.target.parentNode.className) {
            case "timesince":
              basicTranslation_naiveReplace(jQuery(mutationRecord.target.parentNode), "commentingTime", basicTranslation_isTextNode);
              break;
              
            case "btn-text preview-comment top-level-preview-btn":
              basicTranslation_naiveReplace(jQuery(mutationRecord.target.parentNode), "commentingEntryButtons", basicTranslation_isTextNode);
              break;
          } //switch
          
          break;
      } //switch
    } //for
    
    observer.takeRecords();
  }; //onDemand

  /************************************************************************************/
  basicTranslation_naiveReplace = function(nodes, section, filter) {
    
    nodes
      .find(basicTranslation_selectors[section])
      .addBack(basicTranslation_selectors[section])
      .contents()
      .filter(filter)
      .each(function() {
        for (var originPhrase in basicTranslation_dictionary[section]) {
          var destinationPhrase = basicTranslation_dictionary[section][originPhrase];
          this.data = this.data.replace(originPhrase, destinationPhrase);
        }
      });
  }; //routine
</script>

Whereas the (meta-)model has always to be common to both data and process, the code was designed to make a clear distinction between them. This neatly splits it into two parts and saying that this simplifies maintenance does not quite reveal the whole picture: such designs are of clarity (even more thanks to built-in syntax highlighters), which allow non-programmers to grasp and modify the embedded data naturally.

This very favorable property often crystallizes in larger designs, partially inevitably but often not intentionally. It pays off to keep it in focus all the time, it is a characteristic for well done design and here I provide an example how this exists even in the micro-design – producing not only good code but a productive work environment in turn.

High compactness and splits along natural roles pays off – and that does not always mean stronger computational models or hi-tech tools are needed.

Now to the latter half of the code snippet, the process of translation. Upon close inspection, it becomes obvious, that the unification of the apply procedure went a little too high, using an overly simple find-replace logic for everything. This introduces deformation on the semantics of data (cleverly hidden) which by itself presents an impression of straightforwardness. This is a no-go for code which is subject to further re-usage, expansion or deeper embedding.

For the geeks

So much to the code as it is now. Allow now me to show you, the most beautiful structural feature of the previous version, which went lost in the cleanup of the design:

var dictionary = {
  "contactForm": {
    "First Name": "Vorname", 
    "Last Name": "Nachname"
    }, 
  "mobileBar": {
      "Email": "Email", 
      "Call": "Anruf", 
      "Map": "Karte", 
      "Hours": "Zeiten"
    }
  };

for (var translationType in dictionary) {
  for (var originPhrase in dictionary[translationType]) {
    var destinationPhrase = dictionary[translationType][originPhrase];
    
    switch (translationType) {
      case "contactForm":
        jQuery("label.caption:contains('"+originPhrase+"')").contents().each(function(){ if (this.nodeType == 3) { this.data=destinationPhrase; }} );
        break;
        
      case "mobileBar":
        jQuery(".sqs-mobile-info-bar-trigger-label:contains('"+originPhrase+"')").text(destinationPhrase);
        break;
    } //switch
  } //for
} //for

The classical approach is to have separate traversals of the corresponding dictionary parts, the other approach is to split the control flow inside a common traversal. Both approaches have their prerequisites, sanity trip-over points, readability, maintainability, copy-paste properties etc. and I will not go into detail on the decision. I like how the model which ties data and process together turns here inside-out, classifying in data items in the large but providing instructions on branching "inside of leaf items" - which are (have possibility to be) treated uniformly by a common traversal.

There are often just so many details which one could point out and I guess that is exactly why we love programming so much – the transparent structurality which can be revealed in the final product, but even more through the variety which passes by on the way and always surprises; by hard resistance on wrong approach and by fine, cumbersome nuances all the way through.


Comments
No comments yet.