Merge pull request #127 from ricardomaraschini/text-template

Text template
diff --git a/README.md b/README.md
index 187da61..127bdd1 100644
--- a/README.md
+++ b/README.md
@@ -197,9 +197,9 @@
 
 **err.Details()**: *gojsonschema.ErrorDetails* Returns a map[string]interface{} of additional error details specific to the error. For example, GTE errors will have a "min" value, LTE will have a "max" value. See errors.go for a full description of all the error details. Every error always contains a "field" key that holds the value of *err.Field()*
 
-Note in most cases, the err.Details() will be used to generate replacement strings in your locales. and not used directly i.e.
+Note in most cases, the err.Details() will be used to generate replacement strings in your locales, and not used directly. These strings follow the text/template format i.e.
 ```
-%field% must be greater than or equal to %min%
+{{.field}} must be greater than or equal to {{.min}}
 ```
 
 ## Formats
diff --git a/errors.go b/errors.go
index 5146cbb..f22fa65 100644
--- a/errors.go
+++ b/errors.go
@@ -1,10 +1,12 @@
 package gojsonschema
 
 import (
-	"fmt"
-	"strings"
+	"bytes"
+	"text/template"
 )
 
+var errorTemplates *template.Template
+
 type (
 	// RequiredError. ErrorDetails: property string
 	RequiredError struct {
@@ -230,13 +232,32 @@
 	err.SetDescription(formatErrorDescription(d, details))
 }
 
-// formatErrorDescription takes a string in this format: %field% is required
-// and converts it to a string with replacements. The fields come from
-// the ErrorDetails struct and vary for each type of error.
+// formatErrorDescription takes a string in the default text/template
+// format and converts it to a string with replacements. The fields come
+// from the ErrorDetails struct and vary for each type of error.
 func formatErrorDescription(s string, details ErrorDetails) string {
-	for name, val := range details {
-		s = strings.Replace(s, "%"+strings.ToLower(name)+"%", fmt.Sprintf("%v", val), -1)
+
+	var tpl *template.Template
+	var descrAsBuffer bytes.Buffer
+	var err error
+
+	if errorTemplates == nil {
+		errorTemplates = template.New("all-errors")
 	}
 
-	return s
+	tpl = errorTemplates.Lookup(s)
+	if tpl == nil {
+		tpl = errorTemplates.New(s)
+		tpl, err = tpl.Parse(s)
+		if err != nil {
+			return err.Error()
+		}
+	}
+
+	err = tpl.Execute(&descrAsBuffer, details)
+	if err != nil {
+		return err.Error()
+	}
+
+	return descrAsBuffer.String()
 }
diff --git a/locales.go b/locales.go
index ae42ae7..f5698f0 100644
--- a/locales.go
+++ b/locales.go
@@ -84,11 +84,11 @@
 )
 
 func (l DefaultLocale) Required() string {
-	return `%property% is required`
+	return `{{.property}} is required`
 }
 
 func (l DefaultLocale) InvalidType() string {
-	return `Invalid type. Expected: %expected%, given: %given%`
+	return `Invalid type. Expected: {{.expected}}, given: {{.given}}`
 }
 
 func (l DefaultLocale) NumberAnyOf() string {
@@ -108,15 +108,15 @@
 }
 
 func (l DefaultLocale) MissingDependency() string {
-	return `Has a dependency on %dependency%`
+	return `Has a dependency on {{.dependency}}`
 }
 
 func (l DefaultLocale) Internal() string {
-	return `Internal Error %error%`
+	return `Internal Error {{.error}}`
 }
 
 func (l DefaultLocale) Enum() string {
-	return `%field% must be one of the following: %allowed%`
+	return `{{.field}} must be one of the following: {{.allowed}}`
 }
 
 func (l DefaultLocale) ArrayNoAdditionalItems() string {
@@ -128,141 +128,141 @@
 }
 
 func (l DefaultLocale) ArrayMinItems() string {
-	return `Array must have at least %min% items`
+	return `Array must have at least {{.min}} items`
 }
 
 func (l DefaultLocale) ArrayMaxItems() string {
-	return `Array must have at most %max% items`
+	return `Array must have at most {{.max}} items`
 }
 
 func (l DefaultLocale) Unique() string {
-	return `%type% items must be unique`
+	return `{{.type}} items must be unique`
 }
 
 func (l DefaultLocale) ArrayMinProperties() string {
-	return `Must have at least %min% properties`
+	return `Must have at least {{.min}} properties`
 }
 
 func (l DefaultLocale) ArrayMaxProperties() string {
-	return `Must have at most %max% properties`
+	return `Must have at most {{.max}} properties`
 }
 
 func (l DefaultLocale) AdditionalPropertyNotAllowed() string {
-	return `Additional property %property% is not allowed`
+	return `Additional property {{.property}} is not allowed`
 }
 
 func (l DefaultLocale) InvalidPropertyPattern() string {
-	return `Property "%property%" does not match pattern %pattern%`
+	return `Property "{{.property}}" does not match pattern {{.pattern}}`
 }
 
 func (l DefaultLocale) StringGTE() string {
-	return `String length must be greater than or equal to %min%`
+	return `String length must be greater than or equal to {{.min}}`
 }
 
 func (l DefaultLocale) StringLTE() string {
-	return `String length must be less than or equal to %max%`
+	return `String length must be less than or equal to {{.max}}`
 }
 
 func (l DefaultLocale) DoesNotMatchPattern() string {
-	return `Does not match pattern '%pattern%'`
+	return `Does not match pattern '{{.pattern}}'`
 }
 
 func (l DefaultLocale) DoesNotMatchFormat() string {
-	return `Does not match format '%format%'`
+	return `Does not match format '{{.format}}'`
 }
 
 func (l DefaultLocale) MultipleOf() string {
-	return `Must be a multiple of %multiple%`
+	return `Must be a multiple of {{.multiple}}`
 }
 
 func (l DefaultLocale) NumberGTE() string {
-	return `Must be greater than or equal to %min%`
+	return `Must be greater than or equal to {{.min}}`
 }
 
 func (l DefaultLocale) NumberGT() string {
-	return `Must be greater than %min%`
+	return `Must be greater than {{.min}}`
 }
 
 func (l DefaultLocale) NumberLTE() string {
-	return `Must be less than or equal to %max%`
+	return `Must be less than or equal to {{.max}}`
 }
 
 func (l DefaultLocale) NumberLT() string {
-	return `Must be less than %max%`
+	return `Must be less than {{.max}}`
 }
 
 // Schema validators
 func (l DefaultLocale) RegexPattern() string {
-	return `Invalid regex pattern '%pattern%'`
+	return `Invalid regex pattern '{{.pattern}}'`
 }
 
 func (l DefaultLocale) GreaterThanZero() string {
-	return `%number% must be strictly greater than 0`
+	return `{{.number}} must be strictly greater than 0`
 }
 
 func (l DefaultLocale) MustBeOfA() string {
-	return `%x% must be of a %y%`
+	return `{{.x}} must be of a {{.y}}`
 }
 
 func (l DefaultLocale) MustBeOfAn() string {
-	return `%x% must be of an %y%`
+	return `{{.x}} must be of an {{.y}}`
 }
 
 func (l DefaultLocale) CannotBeUsedWithout() string {
-	return `%x% cannot be used without %y%`
+	return `{{.x}} cannot be used without {{.y}}`
 }
 
 func (l DefaultLocale) CannotBeGT() string {
-	return `%x% cannot be greater than %y%`
+	return `{{.x}} cannot be greater than {{.y}}`
 }
 
 func (l DefaultLocale) MustBeOfType() string {
-	return `%key% must be of type %type%`
+	return `{{.key}} must be of type {{.type}}`
 }
 
 func (l DefaultLocale) MustBeValidRegex() string {
-	return `%key% must be a valid regex`
+	return `{{.key}} must be a valid regex`
 }
 
 func (l DefaultLocale) MustBeValidFormat() string {
-	return `%key% must be a valid format %given%`
+	return `{{.key}} must be a valid format {{.given}}`
 }
 
 func (l DefaultLocale) MustBeGTEZero() string {
-	return `%key% must be greater than or equal to 0`
+	return `{{.key}} must be greater than or equal to 0`
 }
 
 func (l DefaultLocale) KeyCannotBeGreaterThan() string {
-	return `%key% cannot be greater than %y%`
+	return `{{.key}} cannot be greater than {{.y}}`
 }
 
 func (l DefaultLocale) KeyItemsMustBeOfType() string {
-	return `%key% items must be %type%`
+	return `{{.key}} items must be {{.type}}`
 }
 
 func (l DefaultLocale) KeyItemsMustBeUnique() string {
-	return `%key% items must be unique`
+	return `{{.key}} items must be unique`
 }
 
 func (l DefaultLocale) ReferenceMustBeCanonical() string {
-	return `Reference %reference% must be canonical`
+	return `Reference {{.reference}} must be canonical`
 }
 
 func (l DefaultLocale) NotAValidType() string {
-	return `%type% is not a valid type -- `
+	return `{{.type}} is not a valid type -- `
 }
 
 func (l DefaultLocale) Duplicated() string {
-	return `%type% type is duplicated`
+	return `{{.type}} type is duplicated`
 }
 
 func (l DefaultLocale) httpBadStatus() string {
-	return `Could not read schema from HTTP, response status is %status%`
+	return `Could not read schema from HTTP, response status is {{.status}}`
 }
 
 // Replacement options: field, description, context, value
 func (l DefaultLocale) ErrorFormat() string {
-	return `%field%: %description%`
+	return `{{.field}}: {{.description}}`
 }
 
 const (