Skip to content
This repository has been archived by the owner on May 2, 2020. It is now read-only.

Commit

Permalink
Closes #649
Browse files Browse the repository at this point in the history
  • Loading branch information
scriptPilot committed Sep 1, 2017
1 parent 8dc073b commit fe4e32e
Show file tree
Hide file tree
Showing 16 changed files with 283 additions and 74 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
<br />

## Next version
### Version 1.13

Released on 2017-09-02

- [x] [#649 - Use language files for multilingual apps](https://github.com/scriptPilot/app-framework/issues/649)

### Version 1.12.1

Expand Down
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ For any open question, feature request or bug, please use our [issue list](https
- [Routing](docs/routing.md)
- [Global data object](docs/data-object.md)
- [sortObject function](docs/sort-object.md)
- [Language files](docs/language-files.md)
- [Images](docs/images.md)
- [Icon fonts](docs/icon-fonts.md)
- [Modules and scripts](docs/modules-and-scripts.md)
Expand Down
33 changes: 33 additions & 0 deletions client/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ let easierGlobalDataObject = {
}
}
}
let easierLanguagePattern = {
methods: {
$lang (key, data = null) {
return this.$root.getLanguagePattern(key, data)
}
}
}

let mixins = {}
mixins.loadConfig = {
Expand Down Expand Up @@ -1129,6 +1136,31 @@ mixins.manageState = {
}
}
}
mixins.languagePattern = {
data: {
languages: [],
languagePattern: {}
},
created () {
this.languages = process.env.LANGUAGES.split(',')
this.languages.forEach((lang) => {
this.languagePattern[lang] = require(process.env.APP_ROOT_FROM_SCRIPTS + 'lang/' + lang + '.json')
})
},
methods: {
getLanguagePattern (key, data) {
const requestedText = this.languagePattern[this.language] ? this.languagePattern[this.language][key] : undefined
const defaultText = this.languagePattern[this.config.defaultLanguage] ? this.languagePattern[this.config.defaultLanguage][key] : undefined
let text = requestedText || defaultText || (data !== null ? '{{' + key + ', ' + JSON.stringify(data) + '}}' : '{{' + key + '}}')
if (data !== null) {
for (let item in data) {
text = text.replace(new RegExp('\\{\\{' + item + '\\}\\}', 'g'), data[item])
}
}
return text
}
}
}

function initF7VueApp () {
// Load Vue
Expand All @@ -1146,6 +1178,7 @@ function initF7VueApp () {
vue.component('image-uploader', require('./image-uploader.vue'))
// Use global mixins
vue.mixin(easierGlobalDataObject)
vue.mixin(easierLanguagePattern)
if (config.restoreComponentData) {
vue.mixin(manageComponentData)
}
Expand Down
6 changes: 5 additions & 1 deletion config-scheme.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
"type": "string",
"default": "My App"
},
"language": {
"defaultLanguage": {
"regexp": "/^[a-z]{2}$/",
"default": "en"
},
"defaultLanguageFallback": {
"type": "boolean",
"default": false
},
"theme": {
"allow": ["ios", "material", "ios-material", "material-ios"],
"default": "material"
Expand Down
3 changes: 2 additions & 1 deletion demo/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"title": "App Framework",
"language": "en",
"defaultLanguage": "en",
"defaultLanguageFallback": false,
"theme": "material-ios",
"color": "indigo",
"layout": "default",
Expand Down
1 change: 1 addition & 0 deletions demo/lang/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
3 changes: 3 additions & 0 deletions demo/pages/home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
let text = this.$$(e.target).find('option[value=' + e.target.value + ']').text()
this.$$(e.target).parent().find('.item-after').html(text)
}.bind(this), 0)
},
testAlert () {
window.f7.alert(this.$lang('message', {username: 'Bugs Bunny'}), this.$lang('subject'))
}
}
}
Expand Down
1 change: 1 addition & 0 deletions docs/folder-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The following project folder will be created by default:
```
├── app/ # App source folder
│ ├── images/ # App images
│ ├── lang/ # App language files
│ ├── pages/ # App page components
│ ├── app.vue # App main component
│ ├── config.js # App configuration
Expand Down
40 changes: 40 additions & 0 deletions docs/language-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Languages Files

> This page is part of the [App Framework Documentation](../DOCUMENTATION.md)
<br />

Languages files provide a simple way to make your app multi-lingual.

## Storage

Language files should be saved in folder *app/lang*. They should be JSON files with plain key:string pairs.

Example `lang/de.json`:

```
{
"emailSubject": "Greetings from Hamburg",
"emailBody": "Hello {{username}}!"
}
```

After you added a new language file, the development server should be restarted.

## Usage

In templates:
- `{{$lang('emailSubject')}}`
- `{{$lang('emailBody', {username: 'Bugs Bunny'})`

In scripts:
- `this.$lang('emailSubject')`
- `this.$lang('emailBody', {username: 'Bugs Bunny'})`

## Default language

To be configured, for example `"defaultLanguage": "en"`. This file is the master file to which all other language files are compared. If there are more keys than in the default language file, an error will be shown on any build command.

## Default language fallback

To be configured, by default `"defaultLanguageFallback": false`. If set to false, an error will be shown if any key is missing compared to the default language file on any build command. If set to true and a key is missing in a secondary language file, the default language is used.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"recursive-readdir-sync": "^1.0.6",
"semver": "^5.3.0",
"shelljs": "^0.7.4",
"sort-keys-recursive": "^2.0.1",
"standard": "^8.6.0",
"to-ico": "^1.1.3",
"underscore": "^1.8.3",
Expand Down
100 changes: 51 additions & 49 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,60 +276,62 @@ function updatePreloader (callback) {

// Run steps
fixCode(function () {
cmd(__dirname, 'node update-routes', function () {
if (mode !== 'dev') {
// Update version in package.json
let pkg = fs.readJsonSync(abs(env.proj, 'package.json'))
pkg.version = ver.inc(pkg.version, mode)
fs.writeJsonSync(abs(env.proj, 'package.json'), pkg)
env.pkg.version = pkg.version
require.cache = {}
}
updateLicense(function () {
updateDocumentation(function () {
cmd(__dirname, 'node update-ignore-files', function () {
fs.emptyDir(abs(env.cache, 'build'), function (err) {
if (err) {
alert('Failed to empty build folder in cache.', 'issue')
} else {
buildWebpack(function () {
cmd(__dirname, 'node create-icons', function () {
manageIcons(function () {
copyFirebaseFiles(function () {
updatePreloader(function () {
// Dev build
if (mode === 'dev') {
alert('Development build done.')
// Pruduction build
} else {
alert('Build folder update ongoing - please wait ...')
// Empty existing build folder
fs.emptyDir(abs(env.proj, 'build'), function (err) {
if (err) {
alert('Clean-up the existing build folder failed.', 'issue')
} else {
// Copy files
fs.copy(abs(env.cache, 'build'), abs(env.proj, 'build'), function (err) {
if (err) {
alert('Build folder update failed.', 'issue')
} else {
compressImages(function () {
// Create snapshot
cmd(__dirname, 'node create-snapshot --name "build-' + env.pkg.version + '"', function () {
alert('Build process done for version ' + env.pkg.version + '.')
cmd(__dirname, 'node checkLanguageFiles', function () {
cmd(__dirname, 'node update-routes', function () {
if (mode !== 'dev') {
// Update version in package.json
let pkg = fs.readJsonSync(abs(env.proj, 'package.json'))
pkg.version = ver.inc(pkg.version, mode)
fs.writeJsonSync(abs(env.proj, 'package.json'), pkg)
env.pkg.version = pkg.version
require.cache = {}
}
updateLicense(function () {
updateDocumentation(function () {
cmd(__dirname, 'node update-ignore-files', function () {
fs.emptyDir(abs(env.cache, 'build'), function (err) {
if (err) {
alert('Failed to empty build folder in cache.', 'issue')
} else {
buildWebpack(function () {
cmd(__dirname, 'node create-icons', function () {
manageIcons(function () {
copyFirebaseFiles(function () {
updatePreloader(function () {
// Dev build
if (mode === 'dev') {
alert('Development build done.')
// Pruduction build
} else {
alert('Build folder update ongoing - please wait ...')
// Empty existing build folder
fs.emptyDir(abs(env.proj, 'build'), function (err) {
if (err) {
alert('Clean-up the existing build folder failed.', 'issue')
} else {
// Copy files
fs.copy(abs(env.cache, 'build'), abs(env.proj, 'build'), function (err) {
if (err) {
alert('Build folder update failed.', 'issue')
} else {
compressImages(function () {
// Create snapshot
cmd(__dirname, 'node create-snapshot --name "build-' + env.pkg.version + '"', function () {
alert('Build process done for version ' + env.pkg.version + '.')
})
})
})
}
})
}
})
}
}
})
}
})
}
})
})
})
})
})
})
}
}
})
})
})
})
Expand Down
76 changes: 76 additions & 0 deletions scripts/checkLanguageFiles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Purpose: Check language files, compare to default language file and sort each file by keys

// Load modules
const env = require('./env')
const alert = require('./alert')
const found = require('./found')
const fs = require('fs-extra')
const path = require('path')
const abs = require('path').resolve
const rec = require('recursive-readdir-sync')
const sort = require('sort-keys-recursive')

// Define language folder
const langFolder = abs(env.app, 'lang')

// Check if default language file exists
const defaultLangFile = abs(langFolder, env.cfg.defaultLanguage + '.json')
if (!found(defaultLangFile)) {
alert(`Default language file lang/${env.cfg.defaultLanguage}.json not found.`, 'error')
}

// Check if all language files are valid and sort each by key
const langPatterns = {}
try {
const files = rec(langFolder)
files.forEach((file) => {
const patterns = fs.readJsonSync(file)
const language = path.basename(file).slice(0, -5)
langPatterns[language] = patterns
fs.writeJsonSync(file, sort(patterns), { spaces: 2 })
})
} catch (err) {
alert('Failed to read language files.', 'issue')
}

// Check all files if they have flat key:string pairs
for (let lang in langPatterns) {
let errors = 0
for (let key in langPatterns[lang]) {
if (typeof langPatterns[lang][key] !== 'string') {
errors++
}
}
if (errors > 0) {
alert(`Language file lang/${lang}.json should contain plain key:string pairs.`, 'error')
}
}

// Compare languages files to default language file
// Loop all languages
for (let lang in langPatterns) {
// It's not the default language
if (lang !== env.cfg.defaultLanguage) {
const diffs = []
// Loop all items of the default language
if (env.cfg.defaultLanguageFallback === false) {
for (let key in langPatterns[env.cfg.defaultLanguage]) {
// Missing items
if (langPatterns[lang][key] === undefined) {
diffs.push(`"${key}" is missing`)
}
}
}
// Loop all items of the second language
for (let key in langPatterns[lang]) {
// Too much items
if (langPatterns[env.cfg.defaultLanguage][key] === undefined) {
diffs.push(`"${key}" is too much`)
}
}
// Differences found
if (diffs.length > 0) {
alert(`Language file lang/${lang}.json is different to the default one:\n- ` + diffs.join('\n- '), 'error')
}
}
}
16 changes: 9 additions & 7 deletions scripts/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,15 @@ function checkFrameworkUpdates (callback) {
alert('Development server preparation ongoing - please wait ...')
fixCode(function () {
checkFrameworkUpdates(function () {
cmd(__dirname, 'node update-routes', function () {
cmd(__dirname, 'node create-icons', function () {
cmd(__dirname, 'node firebase --database --storage --version dev', function () {
startServer(function () {
let uri = 'http://localhost:' + env.cfg.devServerPort
opn(uri)
alert('Development server startet at ' + uri + '.\n\nTo be stopped with "CTRL + C".')
cmd(__dirname, 'node checkLanguageFiles', function () {
cmd(__dirname, 'node update-routes', function () {
cmd(__dirname, 'node create-icons', function () {
cmd(__dirname, 'node firebase --database --storage --version dev', function () {
startServer(function () {
let uri = 'http://localhost:' + env.cfg.devServerPort
opn(uri)
alert('Development server startet at ' + uri + '.\n\nTo be stopped with "CTRL + C".')
})
})
})
})
Expand Down
Loading

0 comments on commit fe4e32e

Please sign in to comment.