feat(ui): plugin locales

This commit is contained in:
Guillaume Chau
2018-04-29 23:05:48 +02:00
parent 9a852d6241
commit a66dabb613
44 changed files with 451 additions and 83 deletions

View File

@@ -10,6 +10,7 @@ This guide will walk you through the development of cli-ui specific features for
- [Shared data](#shared-data)
- [Plugin actions](#plugin-actions)
- [Inter-process communication (IPC)](#inter-process-communication-ipc)
- [Localization](#localization)
- [Hooks](#hooks)
- [Public static files](#public-static-files)
@@ -350,11 +351,19 @@ ClientAddonApi.component('vue-webpack-dashboard', WebpackDashboard)
ClientAddonApi.addRoutes('vue-webpack', [
{ path: '', name: 'test-webpack-route', component: TestView }
])
// You can translate your plugin components
// Load the locale files (uses vue-i18n)
const locales = require.context('./locales', true, /[a-z0-9]+\.json$/i)
locales.keys().forEach(key => {
const locale = key.match(/([a-z0-9]+)\./i)[1]
ClientAddonApi.addLocalization(locale, locales(key))
})
```
The cli-ui registers `Vue` and `ClientAddonApi` as global variables in the `window` scope.
In your components, you can use all the components and the CSS classes of [@vue/ui](https://github.com/vuejs/ui) and [@vue/cli-ui](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-ui/src/components) in order to keep the look and feel consistent.
In your components, you can use all the components and the CSS classes of [@vue/ui](https://github.com/vuejs/ui) and [@vue/cli-ui](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-ui/src/components) in order to keep the look and feel consistent. You can also translate the strings with [vue-i18n](https://github.com/kazupon/vue-i18n) which is included.
#### Register the client addon
Back to the `ui.js` file, use the `api.addClientAddon` method with a require query to the built folder:
@@ -632,6 +641,43 @@ api.ipcSend({
})
```
### Localization
You can put locale files compatible with [vue-i18n](https://github.com/kazupon/vue-i18n) in a `locales` folder at the root of your plugin. They will be automatically loaded into the client when the project is opened. You can then use `$t` to translate strings in your components and other vue-i18n helpers. Also, the strings used in the UI API (like `describeTask`) will go through vue-i18n as well to you can localize them.
Example `locales` folder:
```
vue-cli-plugin/locales/en.json
vue-cli-plugin/locales/fr.json
```
Example usage in API:
```js
api.describeConfig({
// vue-i18n path
description: 'my-plugin.config.foo'
})
```
Example usage in components:
```html
<VueButton>{{ $t('my-plugin.actions.bar') }}</VueButton>
```
You can also load the locale files in a client addon if you prefer, using the `ClientAddonApi`:
```js
// Load the locale files (uses vue-i18n)
const locales = require.context('./locales', true, /[a-z0-9]+\.json$/i)
locales.keys().forEach(key => {
const locale = key.match(/([a-z0-9]+)\./i)[1]
ClientAddonApi.addLocalization(locale, locales(key))
})
```
### Hooks
Hooks allows to react to certain cli-ui events.

View File

@@ -0,0 +1,13 @@
{
"eslint": {
"config": {
"eslint": {
"description": "Error checking & Code quality",
"groups": {
"strongly-recommended": "Strongly recommended",
"recommended": "Recommended"
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
{
"eslint": {
"config": {
"eslint": {
"description": "Vérification des erreurs & Qualité du code",
"groups": {
"strongly-recommended": "Fortement recommandé",
"recommended": "Recommandé"
}
}
}
}
}

View File

@@ -5,7 +5,7 @@ module.exports = api => {
api.describeConfig({
id: 'eslintrc',
name: 'ESLint configuration',
description: 'Error checking & Code quality',
description: 'eslint.config.eslint.description',
link: 'https://eslint.org',
icon: '.eslintrc.json',
files: {
@@ -19,7 +19,7 @@ module.exports = api => {
name: 'vue/attribute-hyphenation',
type: 'list',
message: 'Attribute hyphenation',
group: 'Strongly recommended',
group: 'eslint.config.eslint.groups.strongly-recommended',
description: 'Enforce attribute naming style in template (`my-prop` or `myProp`)',
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/attribute-hyphenation.md',
default: JSON.stringify('off'),
@@ -43,7 +43,7 @@ module.exports = api => {
name: 'vue/html-end-tags',
type: 'confirm',
message: 'Template end tags style',
group: 'Strongly recommended',
group: 'eslint.config.eslint.groups.strongly-recommended',
description: 'End tag on Void elements, end tags and self-closing opening tags',
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-end-tags.md',
default: false,
@@ -55,7 +55,7 @@ module.exports = api => {
name: 'vue/html-indent',
type: 'list',
message: 'Template indentation',
group: 'Strongly recommended',
group: 'eslint.config.eslint.groups.strongly-recommended',
description: 'Enforce indentation in template',
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-indent.md',
default: JSON.stringify('off'),
@@ -87,7 +87,7 @@ module.exports = api => {
name: 'vue/html-self-closing',
type: 'confirm',
message: 'Template tag self-closing style',
group: 'Strongly recommended',
group: 'eslint.config.eslint.groups.strongly-recommended',
description: 'Self-close any component or non-Void element tags',
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-self-closing.md',
default: false,
@@ -99,7 +99,7 @@ module.exports = api => {
name: 'vue/require-default-prop',
type: 'confirm',
message: 'Require default in required props',
group: 'Strongly recommended',
group: 'eslint.config.eslint.groups.strongly-recommended',
description: 'This rule requires default value to be set for each props that are not marked as `required`',
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/require-default-prop.md',
default: false,
@@ -111,7 +111,7 @@ module.exports = api => {
name: 'vue/require-prop-types',
type: 'confirm',
message: 'Require types for props',
group: 'Strongly recommended',
group: 'eslint.config.eslint.groups.strongly-recommended',
description: 'In committed code, prop definitions should always be as detailed as possible, specifying at least type(s)',
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/require-prop-types.md',
default: false,
@@ -123,7 +123,7 @@ module.exports = api => {
name: 'vue/attributes-order',
type: 'confirm',
message: 'Attribute order',
group: 'Recommended',
group: 'eslint.config.eslint.groups.recommended',
description: 'This rule aims to enforce ordering of component attributes (the default order is specified in the Vue style guide)',
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/attributes-order.md',
default: false,
@@ -135,7 +135,7 @@ module.exports = api => {
name: 'vue/html-quotes',
type: 'list',
message: 'Attribute quote style',
group: 'Recommended',
group: 'eslint.config.eslint.groups.recommended',
description: 'Enforce style of the attribute quotes in templates',
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-quotes.md',
default: JSON.stringify('off'),
@@ -159,7 +159,7 @@ module.exports = api => {
name: 'vue/order-in-components',
type: 'confirm',
message: 'Component options order',
group: 'Recommended',
group: 'eslint.config.eslint.groups.recommended',
description: 'This rule aims to enforce ordering of component options (the default order is specified in the Vue style guide)',
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/order-in-components.md',
default: false,

View File

@@ -0,0 +1,18 @@
{
"vue-webpack": {
"dashboard": {
"title": "Dashboard"
},
"analyzer": {
"title": "Analyzer"
},
"tasks": {
"serve": {
"description": "Compiles and hot-reloads for development"
},
"build": {
"description": "Compiles and minifies for production"
}
}
}
}

View File

@@ -0,0 +1,18 @@
{
"vue-webpack": {
"dashboard": {
"title": "Tableau de bord"
},
"analyzer": {
"title": "Analyseur"
},
"tasks": {
"serve": {
"description": "Compile et recharge à chaud pour le développement"
},
"build": {
"description": "Compile et minifie pour la production"
}
}
}
}

View File

@@ -34,13 +34,13 @@ module.exports = api => {
views: [
{
id: 'vue-webpack-dashboard',
label: 'Dashboard',
label: 'vue-webpack.dashboard.title',
icon: 'dashboard',
component: 'vue-webpack-dashboard'
},
{
id: 'vue-webpack-analyzer',
label: 'Analyzer',
label: 'vue-webpack.analyzer.title',
icon: 'donut_large',
component: 'vue-webpack-analyzer'
}
@@ -49,7 +49,7 @@ module.exports = api => {
}
api.describeTask({
match: /vue-cli-service serve/,
description: 'Compiles and hot-reloads for development',
description: 'vue-webpack.tasks.serve.description',
link: 'https://github.com/vuejs/vue-cli/blob/dev/docs/cli-service.md#serve',
prompts: [
{
@@ -121,7 +121,7 @@ module.exports = api => {
})
api.describeTask({
match: /vue-cli-service build/,
description: 'Compiles and minifies for production',
description: 'vue-webpack.tasks.build.description',
link: 'https://github.com/vuejs/vue-cli/blob/dev/docs/cli-service.md#build',
prompts: [
{

View File

@@ -1,7 +1,9 @@
<template>
<div class="asset-list list-block">
<div class="content">
<div class="title">Assets</div>
<div class="title">
{{ $t('vue-webpack.dashboard.asset-list.title') }}
</div>
<VueIcon
v-if="!assetsSorted.length"

View File

@@ -17,7 +17,7 @@
v-if="!asset.secondary && asset.big"
icon="warning"
class="icon"
v-tooltip="'This asset is big, consider using Code splitting to create smaller assets.'"
v-tooltip="$t('vue-webpack.dashboard.asset-list.size-warning')"
/>
</div>
</div>

View File

@@ -2,33 +2,49 @@
<div class="build-status">
<div class="content">
<div class="info-block status">
<div class="label">Status</div>
<div class="value">{{ status || 'Idle' }}</div>
<div class="label">
{{ $t('vue-webpack.dashboard.build-status.labels.status') }}
</div>
<div class="value">{{ $t(`vue-webpack.dashboard.webpack-status.${status || 'Idle'}`) }}</div>
</div>
<div class="info-block errors">
<div class="label">Errors</div>
<div class="label">
{{ $t('vue-webpack.dashboard.build-status.labels.errors') }}
</div>
<div class="value">{{ errors.length }}</div>
</div>
<div class="info-block warnings">
<div class="label">Warnings</div>
<div class="label">
{{ $t('vue-webpack.dashboard.build-status.labels.warnings') }}
</div>
<div class="value">{{ warnings.length }}</div>
</div>
<div class="info-block assets">
<div class="label">Assets</div>
<div class="label">
{{ $t('vue-webpack.dashboard.build-status.labels.assets') }}
</div>
<div class="value">
{{ assetsTotalSize | size('B') }}
<span class="secondary">({{ sizeField }})</span>
<span class="secondary">
({{ $t(`vue-webpack.sizes.${sizeField}`) }})
</span>
</div>
</div>
<div class="info-block modules">
<div class="label">Modules</div>
<div class="label">
{{ $t('vue-webpack.dashboard.build-status.labels.modules') }}
</div>
<div class="value">
{{ modulesTotalSize | size('B') }}
<span class="secondary">({{ sizeField }})</span>
<span class="secondary">
({{ $t(`vue-webpack.sizes.${sizeField}`) }})
</span>
</div>
</div>
<div class="info-block dep-modules">
<div class="label">Dependencies</div>
<div class="label">
{{ $t('vue-webpack.dashboard.build-status.labels.deps') }}
</div>
<div class="value">
{{ depModulesTotalSize | size('B') }}
<span class="secondary">

View File

@@ -1,7 +1,9 @@
<template>
<div class="module-list list-block">
<div class="content">
<div class="title">Dependencies</div>
<div class="title">
{{ $t('vue-webpack.dashboard.module-list.title') }}
</div>
<VueIcon
v-if="!depModules.length"

View File

@@ -1,7 +1,9 @@
<template>
<div class="speed-stats">
<div class="content">
<div class="title">Speed stats</div>
<div class="title">
{{ $t('vue-webpack.dashboard.speed-stats.title') }}
</div>
<VueIcon
v-if="!assetsTotalSize"

View File

@@ -2,18 +2,18 @@
<div class="vue-webpack-analyzer">
<div class="pane-toolbar">
<VueIcon icon="donut_large"/>
<div class="title">Analyzer</div>
<div class="title">{{ $t('vue-webpack.analyzer.title') }}</div>
<template v-if="currentTree">
<VueButton
icon-left="arrow_upward"
label="Go up"
:label="$t('vue-webpack.analyzer.go-up')"
:disabled="currentTree === rootTree"
@click="goToParent()"
/>
<VueButton
icon-left="home"
label="Go to home"
:label="$t('vue-webpack.analyzer.go-home')"
:disabled="currentTree === rootTree"
@click="goToHome()"
/>
@@ -22,23 +22,26 @@
class="separator"
/>
</template>
<VueSelect v-model="selectedChunk">
<VueSelect
v-model="selectedChunk"
:disabled="Object.keys(modulesTrees).length === 0"
>
<VueSelectButton
v-for="(chunk, key) of modulesTrees"
:key="key"
:value="key"
:label="`Chunk ${getChunkName(key)}`"
:label="`${$t('vue-webpack.analyzer.chunk')} ${getChunkName(key)}`"
/>
</VueSelect>
<VueSelect v-model="sizeField">
<VueSelectButton value="stats" label="Stats sizes"/>
<VueSelectButton value="parsed" label="Parsed sizes"/>
<VueSelectButton value="gzip" label="Gzip sizes"/>
<VueSelectButton value="stats" :label="`${$t('vue-webpack.sizes.stats')}`"/>
<VueSelectButton value="parsed" :label="`${$t('vue-webpack.sizes.parsed')}`"/>
<VueSelectButton value="gzip" :label="`${$t('vue-webpack.sizes.gzip')}`"/>
</VueSelect>
<VueButton
class="icon-button"
icon-left="help"
v-tooltip="sizeHelp"
v-tooltip="$t('vue-webpack.sizes.help')"
/>
</div>
@@ -76,19 +79,19 @@
class="stats size"
:class="{ selected: sizeField === 'stats' }"
>
Stats: {{ describedModule.size.stats | size('B')}}
{{ $t('vue-webpack.sizes.stats') }}: {{ describedModule.size.stats | size('B')}}
</div>
<div
class="parsed size"
:class="{ selected: sizeField === 'parsed' }"
>
Parsed: {{ describedModule.size.parsed | size('B')}}
{{ $t('vue-webpack.sizes.parsed') }}: {{ describedModule.size.parsed | size('B')}}
</div>
<div
class="gzip size"
:class="{ selected: sizeField === 'gzip' }"
>
Gzip: {{ describedModule.size.gzip | size('B')}}
{{ $t('vue-webpack.sizes.gzip') }}: {{ describedModule.size.gzip | size('B')}}
</div>
</div>
</div>

View File

@@ -2,14 +2,14 @@
<div class="vue-webpack-dashboard">
<div class="pane-toolbar">
<VueIcon icon="dashboard"/>
<div class="title">Dashboard</div>
<div class="title">{{ $t('vue-webpack.dashboard.title') }}</div>
<template
v-if="mode === 'serve'"
>
<VueButton
icon-left="open_in_browser"
label="Open app"
:label="$t('vue-webpack.dashboard.open-app')"
:disabled="!serveUrl"
@click="$callPluginAction('webpack-dashboard-open-app')"
/>
@@ -19,14 +19,14 @@
/>
</template>
<VueSelect v-model="sizeField">
<VueSelectButton value="stats" label="Stats sizes"/>
<VueSelectButton value="parsed" label="Parsed sizes"/>
<VueSelectButton value="gzip" label="Gzip sizes"/>
<VueSelectButton value="stats" :label="`${$t('vue-webpack.sizes.stats')}`"/>
<VueSelectButton value="parsed" :label="`${$t('vue-webpack.sizes.parsed')}`"/>
<VueSelectButton value="gzip" :label="`${$t('vue-webpack.sizes.gzip')}`"/>
</VueSelect>
<VueButton
class="icon-button"
icon-left="help"
v-tooltip="sizeHelp"
v-tooltip="$t('vue-webpack.sizes.help')"
/>
</div>

View File

@@ -0,0 +1,45 @@
{
"vue-webpack": {
"dashboard": {
"open-app": "Open app",
"webpack-status": {
"Success": "Success",
"Failed": "Failed",
"Compiling": "Compiling",
"Invalidated": "Invalidated",
"Idle": "Idle"
},
"build-status": {
"labels": {
"status": "Status",
"errors": "Errors",
"warnings": "Warnings",
"assets": "Assets",
"modules": "Modules",
"deps": "Dependencies"
}
},
"speed-stats": {
"title": "Speed stats"
},
"module-list": {
"title": "Dependencies"
},
"asset-list": {
"title": "Assets",
"size-warning": "This asset is big, consider using Code splitting to create smaller assets."
}
},
"analyzer": {
"go-up": "Go up",
"go-home": "Go to home",
"chunk": "Chunk"
},
"sizes": {
"stats": "Stats",
"parsed": "Parsed",
"gzip": "Gzip",
"help": "<b>Stats:</b> size from webpack stats data.<br><b>Parsed:</b> size from extracted source (after minification plugins). More accurate.<br><b>Gzip:</b> size of gzipped extracted source."
}
}
}

View File

@@ -0,0 +1,45 @@
{
"vue-webpack": {
"dashboard": {
"open-app": "Ouvrir l'app",
"webpack-status": {
"Success": "Succès",
"Failed": "Echec",
"Compiling": "Compilation...",
"Invalidated": "Invalidé",
"Idle": "Inoccupé"
},
"build-status": {
"labels": {
"status": "Statut",
"errors": "Erreurs",
"warnings": "Avertissements",
"assets": "Fichiers",
"modules": "Modules",
"deps": "Dépendances"
}
},
"speed-stats": {
"title": "Statistiques de vitesse"
},
"module-list": {
"title": "Dépendances"
},
"asset-list": {
"title": "Fichiers",
"size-warning": "Ce fichier est volumineux, vous pouvez réduire sa taille avec le Code-splitting"
}
},
"analyzer": {
"go-up": "Aller au parent",
"go-home": "Aller à la racine",
"chunk": "Chunk"
},
"sizes": {
"stats": "Stats",
"parsed": "Parsé",
"gzip": "Gzip",
"help": "<b>Stats:</b> taille depuis les données statistiques de webpack.<br><b>Parsé:</b> taille depuis les sources extraites (après les plugins de minifications). Plus précis.<br><b>Gzip:</b> taille en source extraites compressée."
}
}
}

View File

@@ -13,3 +13,10 @@ ClientAddonApi.component('vue-webpack-analyzer', WebpackAnalyzer)
ClientAddonApi.addRoutes('vue-webpack', [
{ path: '', name: 'test-webpack-route', component: TestView }
])
// Locales
const locales = require.context('./locales', true, /[a-z0-9]+\.json$/i)
locales.keys().forEach(key => {
const locale = key.match(/([a-z0-9]+)\./i)[1]
ClientAddonApi.addLocalization(locale, locales(key))
})

View File

@@ -30,9 +30,5 @@ export default {
value
})
})
this.sizeHelp = `<b>Stats:</b> size from webpack stats data.<br>
<b>Parsed:</b> size from extracted source (after minification plugins). More accurate.<br>
<b>Gzip:</b> size of gzipped extracted source.`
}
}

View File

@@ -24,6 +24,7 @@
"apollo-link-ws": "^1.0.0",
"apollo-utilities": "^1.0.9",
"clone": "^1.0.4",
"deepmerge": "^2.0.1",
"express-history-api-fallback": "^2.2.1",
"file-icons-js": "^1.0.3",
"graphql": "^0.13.0",

View File

@@ -6,6 +6,7 @@
<StatusBar/>
<ClientAddonLoader/>
<LocaleLoader/>
</div>
</template>

View File

@@ -22,15 +22,10 @@ export default {
}
},
created () {
this.$_scripts = new Map()
},
methods: {
loadAddon (addon) {
console.log(`Loading addon ${addon.id} (${addon.url})...`)
console.log(`[UI] Loading client addon ${addon.id} (${addon.url})...`)
const script = document.createElement('script')
this.$_scripts.set(addon.id, script)
script.setAttribute('src', addon.url)
document.body.appendChild(script)
}

View File

@@ -12,7 +12,7 @@
<ListItemInfo
:name="configuration.name"
:description="configuration.description"
:description="$t(configuration.description)"
:selected="selected"
/>
</div>
@@ -63,7 +63,7 @@ export default {
box-center()
.list-item-info
flex 100% 1 1
flex auto 1 1
width 0
>>> .description

View File

@@ -0,0 +1,38 @@
<script>
import { mergeLocale } from '../i18n'
import LOCALES from '../graphql/locales.gql'
import LOCALE_ADDED from '../graphql/localeAdded.gql'
export default {
apollo: {
locales: {
query: LOCALES,
manual: true,
result ({ data: { locales } }) {
locales.forEach(this.loadLocale)
}
},
$subscribe: {
localeAdded: {
query: LOCALE_ADDED,
result ({ data }) {
this.loadLocale(data.localeAdded)
}
}
}
},
methods: {
loadLocale (locale) {
console.log(`[UI] Locale ${locale.lang} updated with new strings`)
mergeLocale(locale.lang, locale.strings)
}
},
render () {
return null
}
}
</script>

View File

@@ -49,7 +49,7 @@ export default {
@import "~@/style/imports"
.nav-list
overflow-x none
overflow-x hidden
overflow-y auto
background $color-background-light
</style>

View File

@@ -5,8 +5,8 @@
>
<div class="prompt-content">
<ListItemInfo
:name="prompt.message"
:description="prompt.description"
:name="$t(prompt.message)"
:description="$t(prompt.description)"
:link="prompt.link"
/>
@@ -18,7 +18,7 @@
class="right"
@input="value => asnwerCheckbox(choice, value)"
>
{{ choice.name }}
{{ $t(choice.name) }}
</VueSwitch>
</div>

View File

@@ -9,8 +9,8 @@
@input="value => answer(value)"
>
<ListItemInfo
:name="prompt.message"
:description="prompt.description"
:name="$t(prompt.message)"
:description="$t(prompt.description)"
:link="prompt.link"
/>
</VueSwitch>

View File

@@ -2,7 +2,7 @@
<div v-if="error" class="prompt-error">
<div class="vue-ui-text danger banner">
<VueIcon icon="warning" class="big"/>
<span>{{ error.message }}</span>
<span>{{ $t(error.message) }}</span>
</div>
</div>
</template>

View File

@@ -5,8 +5,8 @@
>
<div class="prompt-content">
<ListItemInfo
:name="prompt.message"
:description="prompt.description"
:name="$t(prompt.message)"
:description="$t(prompt.description)"
:link="prompt.link"
/>

View File

@@ -5,8 +5,8 @@
>
<div class="prompt-content">
<ListItemInfo
:name="prompt.message"
:description="prompt.description"
:name="$t(prompt.message)"
:description="$t(prompt.description)"
:link="prompt.link"
/>
@@ -37,7 +37,7 @@ export default {
methods: {
generateLabel (choice) {
let label = choice.name
let label = this.$t(choice.name)
if (choice.isDefault) {
label += ` (${this.$t('components.prompt-list.default')})`
}

View File

@@ -11,5 +11,6 @@ module.exports = {
CLIENT_ADDON_ADDED: 'client_addon_added',
SHARED_DATA_UPDATED: 'shared_data_updated',
PLUGIN_ACTION_CALLED: 'plugin_action_called',
PLUGIN_ACTION_RESOLVED: 'plugin_action_resolved'
PLUGIN_ACTION_RESOLVED: 'plugin_action_resolved',
LOCALE_ADDED: 'locale_added'
}

View File

@@ -2,7 +2,7 @@ const path = require('path')
// Subs
const channels = require('../channels')
// Utils
const { getBasePath } = require('../utils/serve')
const { resolveModuleRoot } = require('../utils/resolve-path')
let addons = []
@@ -43,7 +43,7 @@ function serve (req, res) {
const { id, 0: file } = req.params
const addon = findOne(id)
if (addon && addon.path) {
const basePath = getBasePath(require.resolve(addon.path))
const basePath = resolveModuleRoot(require.resolve(addon.path))
if (basePath) {
res.sendFile(path.join(basePath, file))
return

View File

@@ -0,0 +1,26 @@
// Subs
const channels = require('../channels')
let locales = []
function list (context) {
return locales
}
function add ({ lang, strings }, context) {
const locale = { lang, strings }
locales.push(locale)
context.pubsub.publish(channels.LOCALE_ADDED, {
localeAdded: locale
})
}
function clear (context) {
locales = []
}
module.exports = {
list,
add,
clear
}

View File

@@ -17,6 +17,7 @@ const {
} = require('@vue/cli/lib/util/installDeps')
const invoke = require('@vue/cli/lib/invoke')
const notifier = require('node-notifier')
const globby = require('globby')
// Subs
const channels = require('../channels')
// Connectors
@@ -27,11 +28,12 @@ const progress = require('./progress')
const logs = require('./logs')
const clientAddons = require('./client-addons')
const views = require('./views')
const locales = require('./locales')
// Api
const PluginApi = require('../api/PluginApi')
// Utils
const { getCommand } = require('../utils/command')
const { getBasePath } = require('../utils/serve')
const { resolveModuleRoot } = require('../utils/resolve-path')
const ipc = require('../utils/ipc')
const PROGRESS_ID = 'plugin-installation'
@@ -54,7 +56,7 @@ let installationStep
let projectId
function getPath (id) {
return path.dirname(resolveModule(id, cwd.get()))
return resolveModuleRoot(resolveModule(id, cwd.get()), id)
}
function findPlugins (deps) {
@@ -91,7 +93,7 @@ function resetPluginApi (context) {
// Run Plugin API
runPluginApi('@vue/cli-service', context)
plugins.forEach(plugin => runPluginApi(plugin.id, context))
runPluginApi('.', context, 'vue-cli-ui')
runPluginApi(cwd.get(), context, 'vue-cli-ui')
// Add client addons
pluginApi.clientAddons.forEach(options => clientAddons.add(options, context))
// Add views
@@ -121,6 +123,18 @@ function runPluginApi (id, context, fileName = 'ui') {
module(pluginApi)
pluginApi.pluginId = null
}
// Locales
try {
const folder = fs.existsSync(id) ? id : getPath(id)
const paths = globby.sync([path.join(folder, './locales/*.json')])
paths.forEach(file => {
const basename = path.basename(file)
const lang = basename.substr(0, basename.indexOf('.'))
const strings = JSON.parse(fs.readFileSync(file, { encoding: 'utf8' }))
locales.add({ lang, strings }, context)
})
} catch (e) {}
}
function findOne (id, context) {
@@ -367,7 +381,7 @@ async function callAction ({ id, params }, context) {
function serve (req, res) {
const { id, 0: file } = req.params
const basePath = getBasePath(require.resolve(id), id)
const basePath = id === '.' ? cwd.get() : getPath(id)
if (basePath) {
res.sendFile(path.join(basePath, 'ui-public', file))
return

View File

@@ -14,6 +14,7 @@ const cwd = require('./cwd')
const prompts = require('./prompts')
const folders = require('./folders')
const plugins = require('./plugins')
const locales = require('./locales')
// Context
const getContext = require('../context')
@@ -316,6 +317,8 @@ async function open (id, context) {
lastProject = currentProject
currentProject = project
cwd.set(project.path, context)
// Reset locales
locales.clear()
// Load plugins
plugins.list(project.path, context)

View File

@@ -12,6 +12,7 @@ const progress = require('./connectors/progress')
const files = require('./connectors/files')
const clientAddons = require('./connectors/client-addons')
const sharedData = require('./connectors/shared-data')
const locales = require('./connectors/locales')
// Start ipc server
require('./utils/ipc')
@@ -37,7 +38,8 @@ const resolvers = [{
cwd: () => cwd.get(),
progress: (root, { id }, context) => progress.get(id, context),
clientAddons: (root, args, context) => clientAddons.list(context),
sharedData: (root, { id }, context) => sharedData.get(id, context)
sharedData: (root, { id }, context) => sharedData.get(id, context),
locales: (root, args, context) => locales.list(context)
},
Mutation: {
@@ -73,6 +75,9 @@ const resolvers = [{
(parent, args, { pubsub }) => pubsub.asyncIterator(channels.SHARED_DATA_UPDATED),
(payload, vars) => payload.sharedDataUpdated.id === vars.id
)
},
localeAdded: {
subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(channels.LOCALE_ADDED)
}
}
}]

View File

@@ -54,11 +54,17 @@ type SharedData {
value: JSON
}
type Locale {
lang: String!
strings: JSON!
}
type Query {
progress (id: ID!): Progress
cwd: String!
clientAddons: [ClientAddon]
sharedData (id: ID!): SharedData
locales: [Locale]
}
type Mutation {
@@ -72,6 +78,7 @@ type Subscription {
cwdChanged: String!
clientAddonAdded: ClientAddon
sharedDataUpdated (id: ID!): SharedData
localeAdded: Locale
}
`]

View File

@@ -1,4 +1,4 @@
exports.getBasePath = function (filePath, id = null) {
exports.resolveModuleRoot = function (filePath, id = null) {
{
const index = filePath.lastIndexOf('/index.js')
if (index !== -1) {

View File

@@ -0,0 +1,7 @@
#import "./localeFragment.gql"
subscription localeAdded {
localeAdded {
...locale
}
}

View File

@@ -0,0 +1,4 @@
fragment locale on Locale {
lang
strings
}

View File

@@ -0,0 +1,7 @@
#import "./localeFragment.gql"
query locales {
locales {
...locale
}
}

View File

@@ -1,5 +1,6 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import deepmerge from 'deepmerge'
Vue.use(VueI18n)
@@ -30,4 +31,9 @@ const i18n = new VueI18n({
messages: loadLocaleMessages()
})
export function mergeLocale (lang, messages) {
const newData = deepmerge(i18n.getLocaleMessage(lang), messages)
i18n.setLocaleMessage(lang, newData)
}
export default i18n

View File

@@ -62,7 +62,8 @@
"tooltips": {
"plugins": "Plugins",
"configuration": "Configuration",
"tasks": "Tâches"
"tasks": "Tâches",
"more": "Plus"
}
},
"project-select-list": {
@@ -275,6 +276,12 @@
"project-configurations": {
"title": "Configuration du projet"
},
"project-configuration-details": {
"actions": {
"cancel": "Annuler les changements",
"save": "Sauvegarder les modifications"
}
},
"project-tasks": {
"title": "Tâches du projet"
},
@@ -288,6 +295,13 @@
"parameters": "Paramètres",
"more-info": "Plus d'infos",
"output": "Sortie"
},
"about": {
"title": "A propos",
"description": "<a href=\"https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-ui\" target=\"_blank\">@vue/cli-ui</a> est un paquet inclus dans @vue/cli qui affiche une interface graphique.",
"quote": "Vue-cli 3.x is a complete rewrite, with a lot of new awesome features. You will be to select features like routing, Vuex or Typescript, then add and upgrade building blocks called \"vue-cli plugins\". But having so much more options also means the tool is now more complex and harder to start using. That's why we thought having a full-blown GUI would help discover the new features, search and install vue-cli plugins and unlock more possibilities overall while not being limited by a terminal interface. To sum up, vue-cli will not only allow you to bootstrap a new project easily, but it will also remain useful for ongoing work afterwards!",
"links": "Liens utiles",
"back": "Retour"
}
}
}

View File

@@ -1,5 +1,6 @@
import Vue from 'vue'
import router from '../router'
import { mergeLocale } from '../i18n'
import ProjectHome from '../views/ProjectHome.vue'
export default class ClientAddonApi {
@@ -18,7 +19,7 @@ export default class ClientAddonApi {
this.components.set(id, definition)
const componentId = toComponentId(id)
Vue.component(componentId, definition)
console.log(`Registered ${componentId} component`)
console.log(`[ClientAddonApi] Registered ${componentId} component`)
// Call listeners
const listeners = this.componentListeners.get(id)
if (listeners) {
@@ -47,6 +48,18 @@ export default class ClientAddonApi {
children: routes
}
])
console.log(`[ClientAddonApi] Registered new routes under the /addon/${id} route`)
}
/**
* Merge new strings into the specified lang translations (using vue-i18n).
*
* @param {string} lang Locale to merge to (ex: 'en', 'fr'...)
* @param {object} strings A vue-i18n strings object containing the translations
*/
addLocalization (lang, strings) {
mergeLocale(lang, strings)
console.log(`[ClientAddonApi] Registered new strings for locale ${lang}`)
}
/* Internal */

View File

@@ -4,7 +4,7 @@
<div class="header">
<VueIcon icon="assignment" class="task-icon big"/>
<div class="name">{{ task.name }}</div>
<div class="description">{{ task.description }}</div>
<div class="description">{{ $t(task.description) }}</div>
</div>
<div class="actions-bar">
@@ -66,7 +66,7 @@
:key="view.id"
:value="view.id"
:icon-left="view.icon"
:label="view.label"
:label="$t(view.label)"
/>
</VueGroup>
</div>