You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-08-08 22:36:41 +02:00
tools/text-translator: new extractor tool & README.md
This commit is contained in:
@ -5,5 +5,66 @@ twins@geeksaccelerator.com
|
||||
|
||||
|
||||
## Description
|
||||
_text-translator_ is a tool for automatic translation of messages using
|
||||
AWS Translator service and universal-translator json files.
|
||||
_text-translator_ is a set of two tools: `extractor` and `translator`. Their goal is assisting in
|
||||
automatically generating resources to add support for internazionalied go-templates.
|
||||
|
||||
Theses tools aren't required for the build pipeline, but it might help in saving some boilerplate
|
||||
if you have many go-templates which harcoded messages in English.
|
||||
|
||||
In order to have a feeling of how to use these tools, see the next section which goes through a
|
||||
complete use-case for them.
|
||||
|
||||
## Usage
|
||||
To understand how to use them, let's consider the `signup-step1.gohtml` from `web-app` which you
|
||||
can find in `cmd/web-app/content/`.
|
||||
This template has hardcoded text in English.
|
||||
|
||||
If you visit `http://localhost:3000/signup`:
|
||||

|
||||
|
||||
If you try to specify a custom `locale` with `http://localhost:3000/signup?locale=fr` or `http://localhost:3000/signup?locale=es` you will see the same webpage since English text is harcoded in the template.
|
||||
|
||||
Lets start by using the `extractor` tool to save us some time
|
||||
extracting harcoded texts to their corresponding `en` .json files
|
||||
which will be used by `universal-translator`.
|
||||
|
||||
Go to `tools/text-translator/extractor` and run:
|
||||
```
|
||||
go run main.go -i ../../../../cmd/web-app/templates/content/signup-step1.gohtml -o ../../../../cmd/web-app/templates/content/translations
|
||||
```
|
||||
This command takes the `signup-step1.gohtml` template file and generates a `universal-translator` file `cmd/web-app-templates/content/translations/en/signup-step1.json` with the extracted english texts.
|
||||
|
||||
Now we should use the generated place holders in the `.gohtml` file. Currently this should be done manually, since the go-templates
|
||||
files aren't pure html (the underlying parser of the tool is `net/html`). This makes automatic replacement of found texts
|
||||
a bit harder since the files aren't 100% valid.
|
||||
|
||||
You can look at this particular example; the original and manually transformed template in `tools/cmd/extractor/.example.original.signup-step1.gohtml` and `tools/cmd/extractor/.example.transformed.signup-step1.gohtml`.
|
||||
|
||||
Since now the `.gohtml` uses the `universal-translator` through the `{{ $.trans.T <placeholder> }}` action, we should add proper support for other languages. For this, we'll leverage the `translator` tool to generate json files for other locales using the now existing `en` texts.
|
||||
|
||||
Now move to `tool/text-translator/cmd/translator` and run:
|
||||
```
|
||||
go run main.go -i ../../../../cmd/web-app/templates/content/translations/en/signup-step1.json -o ../../../../cmd/web-app/templates/content/translations -t fr,zh
|
||||
```
|
||||
The `extractor` will read the `en/signup/step1.json` and use `AWS Translator` to generate proper `.json` files for `fr` and `zh` locales. Remember that you should properly have configured env variables or the config folder with AWS credentials (`AWS_ACCESS_KEY`, `AWS_SECRET_ACCESS_KEY`, and region `AWS_DEFAULT_REGION`).
|
||||
|
||||
Now if you enter `http://localhost:3000?signup?locale=fr`, you'd see:
|
||||

|
||||
|
||||
And `http://localhost:3000?signup?locale=zh`:
|
||||

|
||||
|
||||
Notice an important point: In this example there're some fields such
|
||||
as `Zipcode` and `Region` which didn't get extracted by `extractor`. In this case is because the HTML for these fields is generated dynamically with javascript, so it gets missed. This is one example of the border cases you should pay attention to.
|
||||
|
||||
## Caveats
|
||||
These tools are still in its infancy and they have a lot of room for improvement. While they might help aliviating a lot of word with extracting, replacing and translating tasks, its results aren't flawless. In these three steps there may be unwanted outputs:
|
||||
* all harcoded go-template texts might not be extracted
|
||||
* some extracted texts might have details to fine-tune manually
|
||||
* even if `AWS Translator` service is quite good, it may not be perfect
|
||||
|
||||
It's highly recommendable that you quickly look through the results and finish
|
||||
perfecting the result. Using `git` to properly see what changed in the `.gohtml` file, and verifying it was appropiate, would be a good advice to have in mind.
|
||||
|
||||
## Contribute!
|
||||
We're open for contributions to improve these tools and make them better!
|
@ -0,0 +1,251 @@
|
||||
{{define "title"}}Create an Account{{end}}
|
||||
{{define "description"}}Sign Up for free to our Software-as-a-Service solution. {{end}}
|
||||
{{define "style"}}
|
||||
|
||||
{{end}}
|
||||
{{ define "partials/app-wrapper" }}
|
||||
<div class="container" id="page-content">
|
||||
|
||||
<div class="card o-hidden border-0 shadow-lg my-5">
|
||||
<div class="card-body p-0">
|
||||
<!-- Nested Row within Card Body -->
|
||||
<div class="row">
|
||||
<div class="col-lg-5 d-none d-lg-block bg-register-image"></div>
|
||||
<div class="col-lg-7">
|
||||
<div class="p-5">
|
||||
{{ template "app-flashes" . }}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="h4 text-gray-900 mb-4">Create an Account!</h1>
|
||||
</div>
|
||||
|
||||
{{ template "validation-error" . }}
|
||||
|
||||
<hr>
|
||||
<form class="user" method="post" novalidate>
|
||||
|
||||
<div>
|
||||
<h2 class="h5 text-gray-900 mt-3 mb-3">Your Organization details</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Name" }}"
|
||||
name="Account.Name" value="{{ $.form.Account.Name }}" placeholder="Company Name" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Name" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Address1" }}"
|
||||
name="Account.Address1" value="{{ $.form.Account.Address1 }}" placeholder="Address Line 1" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Address1" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Address2" }}"
|
||||
name="Account.Address2" value="{{ $.form.Account.Address2 }}" placeholder="Address Line 2">
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Address2" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<div class="form-control-select-wrapper">
|
||||
<select id="selectAccountCountry" name="Account.Country" placeholder="Country" required
|
||||
class="form-control form-control-select-box {{ ValidationFieldClass $.validationErrors "Account.Country" }}">
|
||||
{{ range $i := $.countries }}
|
||||
{{ $hasGeonames := false }}
|
||||
{{ range $c := $.geonameCountries }}
|
||||
{{ if eq $c $i.Code }}{{ $hasGeonames = true }}{{ end }}
|
||||
{{ end }}
|
||||
<option value="{{ $i.Code }}" data-geonames="{{ if $hasGeonames }}1{{ else }}0{{ end }}" {{ if eq $.form.Account.Country $i.Code }}selected="selected"{{ end }}>{{ $i.Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Country" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<div id="divAccountZipcode"></div>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Zipcode" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<div id="divAccountRegion"></div>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Region" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-4">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="text" id="inputAccountCity"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.City" }}"
|
||||
name="Account.City" value="{{ $.form.Account.City }}" placeholder="City" required>
|
||||
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.City" }}
|
||||
</div>
|
||||
<!-- div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<select class="form-control {{ ValidationFieldClass $.validationErrors "Account.Timezone" }}" id="selectAccountTimezone" name="Account.Timezone" placeholder="Timezone"></select>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Timezone" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div -->
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h2 class="h5 text-gray-900 mt-3 mb-3">Your User details</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.FirstName" }}"
|
||||
name="User.FirstName" value="{{ $.form.User.FirstName }}" placeholder="First Name" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.FirstName" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.LastName" }}"
|
||||
name="User.LastName" value="{{ $.form.User.LastName }}" placeholder="Last Name" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.LastName" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="email"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.Email" }}"
|
||||
name="User.Email" value="{{ $.form.User.Email }}" placeholder="Email Address" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.Email" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="password"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.Password" }}"
|
||||
name="User.Password" value="{{ $.form.User.Password }}" placeholder="Password" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.Password" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input type="password"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.PasswordConfirm" }}"
|
||||
name="User.PasswordConfirm" value="{{ $.form.User.PasswordConfirm }}" placeholder="Repeat Password" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.PasswordConfirm" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-user btn-block">
|
||||
Register Account
|
||||
</button>
|
||||
|
||||
</form>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<a class="small" href="/user/login">Already have an account? Login!</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "js"}}
|
||||
<script src="https://cdn.jsdelivr.net/gh/xcash/bootstrap-autocomplete@v2.2.2/dist/latest/bootstrap-autocomplete.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$(document).find('body').addClass('bg-gradient-primary');
|
||||
|
||||
$('#selectAccountCountry').on('change', function () {
|
||||
|
||||
// When a country has data-geonames, then we can perform autocomplete on zipcode and
|
||||
// populate a list of valid regions.
|
||||
if ($(this).find('option:selected').attr('data-geonames') == 1) {
|
||||
|
||||
// Replace the existing region with an empty dropdown.
|
||||
$('#divAccountRegion').html('<div class="form-control-select-wrapper"><select class="form-control form-control-select-box {{ ValidationFieldClass $.validationErrors "Account.Region" }}" id="inputAccountRegion" name="Account.Region" value="{{ $.form.Account.Region }}" placeholder="Region" required></select></div>');
|
||||
|
||||
// Query the API for a list of regions for the selected
|
||||
// country and populate the region dropdown.
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
url: '/geo/regions/autocomplete',
|
||||
data: {country_code: $(this).val(), select: true},
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
if (res !== undefined && res !== null) {
|
||||
for (var c in res) {
|
||||
$('#inputAccountRegion').append('<option value="'+res[c].value+'">'+res[c].text+'</option>');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
// Remove all the existing items from the timezone dropdown and repopulate it.
|
||||
$('#selectAccountTimezone').find('option').remove().end()
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
url: '/geo/country/'+$(this).val()+'/timezones',
|
||||
data: {},
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
if (res !== undefined && res !== null) {
|
||||
for (var c in res) {
|
||||
$('#selectAccountTimezone').append('<option value="'+res[c]+'">'+res[c]+'</option>');
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// Replace the existing zipcode text input with a new one that will supports autocomplete.
|
||||
$('#divAccountZipcode').html('<input class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Zipcode" }}" id="inputAccountZipcode" name="Account.Zipcode" value="{{ $.form.Account.Zipcode }}" placeholder="Zipcode" required>');
|
||||
$('#inputAccountZipcode').autoComplete({
|
||||
minLength: 2,
|
||||
events: {
|
||||
search: function (qry, callback) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
url: '/geo/postal_codes/autocomplete',
|
||||
data: {query: qry, country_code: $('#selectAccountCountry').val()},
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
callback(res)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// When the value of zipcode changes, try to find an exact match for the zipcode and
|
||||
// can therefore set the correct region and city.
|
||||
$('#inputAccountZipcode').on('change', function() {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
url: '/geo/geonames/postal_code/'+$(this).val(),
|
||||
data: {country_code: $('#selectAccountCountry').val()},
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
if (res !== undefined && res !== null && res.PostalCode !== undefined) {
|
||||
$('#inputAccountCity').val(res.PlaceName);
|
||||
$('#inputAccountRegion').val(res.StateCode);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// Replace the existing zipcode input with no autocomplete.
|
||||
$('#divAccountZipcode').html('<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Zipcode" }}" id="inputAccountZipcode" name="Account.Zipcode" value="{{ $.form.Account.Zipcode }}" placeholder="Zipcode" required>');
|
||||
|
||||
// Replace the existing region select with a text input.
|
||||
$('#divAccountRegion').html('<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Region" }}" id="inputAccountRegion" name="Account.Region" value="{{ $.form.Account.Region }}" placeholder="Region" required>');
|
||||
|
||||
}
|
||||
}).change();
|
||||
|
||||
hideDuplicateValidationFieldErrors();
|
||||
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
@ -0,0 +1,251 @@
|
||||
{{define "title"}}{{ $.trans.T "signup-step1-title" }}{{end}}
|
||||
{{define "description"}}{{ $.trans.T "signup-step1-description" }}{{end}}
|
||||
{{define "style"}}
|
||||
|
||||
{{end}}
|
||||
{{ define "partials/app-wrapper" }}
|
||||
<div class="container" id="page-content">
|
||||
|
||||
<div class="card o-hidden border-0 shadow-lg my-5">
|
||||
<div class="card-body p-0">
|
||||
<!-- Nested Row within Card Body -->
|
||||
<div class="row">
|
||||
<div class="col-lg-5 d-none d-lg-block bg-register-image"></div>
|
||||
<div class="col-lg-7">
|
||||
<div class="p-5">
|
||||
{{ template "app-flashes" . }}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="h4 text-gray-900 mb-4">{{ $.trans.T "signup-step1-create-an-account" }}</h1>
|
||||
</div>
|
||||
|
||||
{{ template "validation-error" . }}
|
||||
|
||||
<hr>
|
||||
<form class="user" method="post" novalidate>
|
||||
|
||||
<div>
|
||||
<h2 class="h5 text-gray-900 mt-3 mb-3">{{ $.trans.T "signup-step1-your-organization-details" }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Name" }}"
|
||||
name="Account.Name" value="{{ $.form.Account.Name }}" placeholder="{{ $.trans.T "signup-step1-company-name" }}" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Name" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Address1" }}"
|
||||
name="Account.Address1" value="{{ $.form.Account.Address1 }}" placeholder="{{ $.trans.T "signup-step1-address-line-1" }}" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Address1" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Address2" }}"
|
||||
name="Account.Address2" value="{{ $.form.Account.Address2 }}" placeholder="{{ $.trans.T "signup-step1-address-line-2" }}">
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Address2" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<div class="form-control-select-wrapper">
|
||||
<select id="selectAccountCountry" name="Account.Country" placeholder="{{ $.trans.T "signup-step1-country" }}" required
|
||||
class="form-control form-control-select-box {{ ValidationFieldClass $.validationErrors "Account.Country" }}">
|
||||
{{ range $i := $.countries }}
|
||||
{{ $hasGeonames := false }}
|
||||
{{ range $c := $.geonameCountries }}
|
||||
{{ if eq $c $i.Code }}{{ $hasGeonames = true }}{{ end }}
|
||||
{{ end }}
|
||||
<option value="{{ $i.Code }}" data-geonames="{{ if $hasGeonames }}1{{ else }}0{{ end }}" {{ if eq $.form.Account.Country $i.Code }}selected="selected"{{ end }}>{{ $i.Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Country" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<div id="divAccountZipcode"></div>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Zipcode" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<div id="divAccountRegion"></div>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Region" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-4">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="text" id="inputAccountCity"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.City" }}"
|
||||
name="Account.City" value="{{ $.form.Account.City }}" placeholder="{{ $.trans.T "signup-step1-city" }}" required>
|
||||
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.City" }}
|
||||
</div>
|
||||
<!-- div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<select class="form-control {{ ValidationFieldClass $.validationErrors "Account.Timezone" }}" id="selectAccountTimezone" name="Account.Timezone" placeholder="Timezone"></select>
|
||||
{{template "invalid-feedback" dict "fieldName" "Account.Timezone" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div -->
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h2 class="h5 text-gray-900 mt-3 mb-3">{{ $.trans.T "signup-step1-your-user-details" }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.FirstName" }}"
|
||||
name="User.FirstName" value="{{ $.form.User.FirstName }}" placeholder="{{ $.trans.T "signup-step1-first-name" }}" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.FirstName" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input type="text"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.LastName" }}"
|
||||
name="User.LastName" value="{{ $.form.User.LastName }}" placeholder="{{ $.trans.T "signup-step1-last-name" }}" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.LastName" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="email"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.Email" }}"
|
||||
name="User.Email" value="{{ $.form.User.Email }}" placeholder="{{ $.trans.T "signup-step1-email-address" }}" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.Email" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||
<input type="password"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.Password" }}"
|
||||
name="User.Password" value="{{ $.form.User.Password }}" placeholder="{{ $.trans.T "signup-step1-password" }}" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.Password" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input type="password"
|
||||
class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.PasswordConfirm" }}"
|
||||
name="User.PasswordConfirm" value="{{ $.form.User.PasswordConfirm }}" placeholder="{{ $.trans.T "signup-step1-repeat-password" }}" required>
|
||||
{{template "invalid-feedback" dict "fieldName" "User.PasswordConfirm" "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-user btn-block">
|
||||
{{ $.trans.T "signup-step1-register-account" }}
|
||||
</button>
|
||||
|
||||
</form>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<a class="small" href="/user/login">{{ $.trans.T "signup-step1-already-have-an-account" }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "js"}}
|
||||
<script src="https://cdn.jsdelivr.net/gh/xcash/bootstrap-autocomplete@v2.2.2/dist/latest/bootstrap-autocomplete.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$(document).find('body').addClass('bg-gradient-primary');
|
||||
|
||||
$('#selectAccountCountry').on('change', function () {
|
||||
|
||||
// When a country has data-geonames, then we can perform autocomplete on zipcode and
|
||||
// populate a list of valid regions.
|
||||
if ($(this).find('option:selected').attr('data-geonames') == 1) {
|
||||
|
||||
// Replace the existing region with an empty dropdown.
|
||||
$('#divAccountRegion').html('<div class="form-control-select-wrapper"><select class="form-control form-control-select-box {{ ValidationFieldClass $.validationErrors "Account.Region" }}" id="inputAccountRegion" name="Account.Region" value="{{ $.form.Account.Region }}" placeholder="Region" required></select></div>');
|
||||
|
||||
// Query the API for a list of regions for the selected
|
||||
// country and populate the region dropdown.
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
url: '/geo/regions/autocomplete',
|
||||
data: {country_code: $(this).val(), select: true},
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
if (res !== undefined && res !== null) {
|
||||
for (var c in res) {
|
||||
$('#inputAccountRegion').append('<option value="'+res[c].value+'">'+res[c].text+'</option>');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
// Remove all the existing items from the timezone dropdown and repopulate it.
|
||||
$('#selectAccountTimezone').find('option').remove().end()
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
url: '/geo/country/'+$(this).val()+'/timezones',
|
||||
data: {},
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
if (res !== undefined && res !== null) {
|
||||
for (var c in res) {
|
||||
$('#selectAccountTimezone').append('<option value="'+res[c]+'">'+res[c]+'</option>');
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// Replace the existing zipcode text input with a new one that will supports autocomplete.
|
||||
$('#divAccountZipcode').html('<input class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Zipcode" }}" id="inputAccountZipcode" name="Account.Zipcode" value="{{ $.form.Account.Zipcode }}" placeholder="Zipcode" required>');
|
||||
$('#inputAccountZipcode').autoComplete({
|
||||
minLength: 2,
|
||||
events: {
|
||||
search: function (qry, callback) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
url: '/geo/postal_codes/autocomplete',
|
||||
data: {query: qry, country_code: $('#selectAccountCountry').val()},
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
callback(res)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// When the value of zipcode changes, try to find an exact match for the zipcode and
|
||||
// can therefore set the correct region and city.
|
||||
$('#inputAccountZipcode').on('change', function() {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
url: '/geo/geonames/postal_code/'+$(this).val(),
|
||||
data: {country_code: $('#selectAccountCountry').val()},
|
||||
dataType: 'json'
|
||||
}).done(function (res) {
|
||||
if (res !== undefined && res !== null && res.PostalCode !== undefined) {
|
||||
$('#inputAccountCity').val(res.PlaceName);
|
||||
$('#inputAccountRegion').val(res.StateCode);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// Replace the existing zipcode input with no autocomplete.
|
||||
$('#divAccountZipcode').html('<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Zipcode" }}" id="inputAccountZipcode" name="Account.Zipcode" value="{{ $.form.Account.Zipcode }}" placeholder="Zipcode" required>');
|
||||
|
||||
// Replace the existing region select with a text input.
|
||||
$('#divAccountRegion').html('<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Region" }}" id="inputAccountRegion" name="Account.Region" value="{{ $.form.Account.Region }}" placeholder="Region" required>');
|
||||
|
||||
}
|
||||
}).change();
|
||||
|
||||
hideDuplicateValidationFieldErrors();
|
||||
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
196
tools/text-translator/cmd/extractor/main.go
Normal file
196
tools/text-translator/cmd/extractor/main.go
Normal file
@ -0,0 +1,196 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"geeks-accelerator/oss/saas-starter-kit/tools/text-translator/internal/jsontranslator"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
const (
|
||||
keyWordLengthLimit = 4
|
||||
)
|
||||
|
||||
var (
|
||||
inFile = flag.String("i", "", "source .gohtml file to extract text from")
|
||||
outDir = flag.String("o", "", "output directory for translations")
|
||||
locale = flag.String("l", "en", "locale of input file")
|
||||
|
||||
allowedCharsKeyRegex, _ = regexp.Compile("[^a-zA-Z0-9 ]+")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
flag.VisitAll(func(f *flag.Flag) {
|
||||
if f.Value.String() == "" {
|
||||
fmt.Printf("-%s flag is required\n", f.Name)
|
||||
os.Exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
s, err := os.Stat(*inFile)
|
||||
if err != nil {
|
||||
fmt.Printf("coudn't check if path is a file or directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if s.IsDir() {
|
||||
filepath.Walk(*inFile, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Printf("error while walking path: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
parseFile(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
parseFile(*inFile)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func parseFile(path string) {
|
||||
log.Printf("reading file %s\n", path)
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Printf("error while reading input file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
content := string(b)
|
||||
_, name := filepath.Split(path)
|
||||
filenameWithoutExt := strings.TrimRight(name, filepath.Ext(name))
|
||||
|
||||
translationFile := jsontranslator.JSONTranslation{Locale: *locale}
|
||||
|
||||
// Extract title and description
|
||||
title, description := extractTitleAndDescription(content)
|
||||
if title != "" {
|
||||
translationFile.Items = append(translationFile.Items, jsontranslator.Translation{
|
||||
Locale: *locale,
|
||||
Key: fmt.Sprintf("%s-title", filenameWithoutExt),
|
||||
Translation: title,
|
||||
})
|
||||
}
|
||||
if description != "" {
|
||||
translationFile.Items = append(translationFile.Items, jsontranslator.Translation{
|
||||
Locale: *locale,
|
||||
Key: fmt.Sprintf("%s-description", filenameWithoutExt),
|
||||
Translation: description,
|
||||
})
|
||||
}
|
||||
|
||||
// Extract texts from html tags
|
||||
extractedTexts := unique(extract(content, filenameWithoutExt))
|
||||
for _, text := range extractedTexts {
|
||||
key := makeKey(text, filenameWithoutExt)
|
||||
translationFile.Items = append(translationFile.Items, jsontranslator.Translation{
|
||||
Locale: *locale,
|
||||
Key: key,
|
||||
Translation: text,
|
||||
})
|
||||
}
|
||||
|
||||
err = jsontranslator.Save(*outDir, filenameWithoutExt+".json", []jsontranslator.JSONTranslation{translationFile})
|
||||
if err != nil {
|
||||
fmt.Printf("error while saving the extracted strings: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func extract(content string, keyPrefix string) []string {
|
||||
var res []string
|
||||
r := strings.NewReader(content)
|
||||
doc, err := goquery.NewDocumentFromReader(r)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f := func(text string) {
|
||||
// Safemode: avoid contents that involve template actions
|
||||
if strings.Index(text, "{{") != -1 {
|
||||
return
|
||||
}
|
||||
if len(text) > 0 {
|
||||
res = append(res, text)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract <input> placeholder texts
|
||||
doc.Find("input").Union(doc.Find("select")).Each(func(i int, input *goquery.Selection) {
|
||||
if p, exists := input.Attr("placeholder"); exists {
|
||||
f(p)
|
||||
}
|
||||
})
|
||||
|
||||
// Extract text from <p>, <a>, ... tags
|
||||
simpleTags := []string{"p", "a", "h1", "h2", "h3", "h4", "h5", "h6", "button", "small", "label", "li"}
|
||||
n := &goquery.Selection{}
|
||||
for _, tag := range simpleTags {
|
||||
n = n.Union(doc.Find(tag))
|
||||
}
|
||||
n.Each(func(i int, n *goquery.Selection) {
|
||||
if n.Children().Length() == 0 {
|
||||
f(n.Text())
|
||||
}
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func extractTitleAndDescription(content string) (title, description string) {
|
||||
idxStart := strings.Index(content, `{{define "title"}}`)
|
||||
if idxStart >= 0 {
|
||||
idxEnd := strings.Index(content, `{{end}}`)
|
||||
if idxEnd >= 0 {
|
||||
title = content[idxStart+18 : idxEnd]
|
||||
content = content[idxEnd+7:]
|
||||
}
|
||||
}
|
||||
|
||||
idxStart = strings.Index(content, `{{define "description"}}`)
|
||||
if idxStart >= 0 {
|
||||
idxEnd := strings.Index(content, `{{end}}`)
|
||||
if idxEnd >= 0 {
|
||||
description = content[idxStart+24 : idxEnd]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeKey(text, prefix string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
key := allowedCharsKeyRegex.ReplaceAllString(text, "")
|
||||
key = strings.ToLower(key)
|
||||
|
||||
split := strings.SplitN(key, " ", keyWordLengthLimit+1)
|
||||
limit := len(split)
|
||||
if limit > keyWordLengthLimit {
|
||||
limit = keyWordLengthLimit
|
||||
}
|
||||
key = strings.Join(split[:limit], "-")
|
||||
|
||||
key = fmt.Sprintf("%s-%s", prefix, key)
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func unique(lst []string) []string {
|
||||
keys := make(map[string]struct{})
|
||||
list := []string{}
|
||||
for _, s := range lst {
|
||||
if _, exist := keys[s]; !exist {
|
||||
keys[s] = struct{}{}
|
||||
list = append(list, s)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
Reference in New Issue
Block a user