@plutocms/pluto 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/FEATURES.md +7 -0
- package/app/app.vue +5 -2
- package/app/components/ColorModeButton.vue +2 -1
- package/app/components/Pluto/Content/PlutoContentField.vue +17 -3
- package/app/components/Pluto/Content/PlutoContentForm.vue +60 -38
- package/app/components/Pluto/Content/PlutoContentList.vue +38 -32
- package/app/components/Pluto/Content/fields/PlutoFieldReference.vue +2 -1
- package/app/components/Pluto/Dashboard/PlutoDashboardContentActivity.vue +22 -19
- package/app/components/Pluto/Dashboard/PlutoDashboardContentOverview.vue +15 -14
- package/app/components/Pluto/Dashboard/PlutoDashboardQuickActions.vue +6 -5
- package/app/components/Pluto/PlutoDashboardWidgets.vue +2 -1
- package/app/components/Pluto/PlutoRoot.vue +18 -1
- package/app/components/Pluto/Settings/PlutoSettingsLanguage.vue +51 -0
- package/app/components/modal/ModalHeader.vue +3 -1
- package/app/composables/internal/content-text.ts +111 -0
- package/app/composables/internal/error-text.ts +58 -0
- package/app/composables/internal/i18n-core.ts +310 -0
- package/app/composables/internal/i18n-runtime.ts +87 -0
- package/app/composables/pluto-admin-nav.ts +21 -4
- package/app/composables/pluto-admin-pages.ts +13 -3
- package/app/composables/pluto-command-palette.ts +13 -6
- package/app/composables/pluto-content-actions.ts +9 -5
- package/app/composables/pluto-content-text.ts +28 -0
- package/app/composables/pluto-dashboard.ts +15 -2
- package/app/composables/pluto-error.ts +13 -0
- package/app/composables/pluto-extension.ts +4 -0
- package/app/composables/pluto-i18n.ts +171 -0
- package/app/composables/pluto-permissions.ts +16 -2
- package/app/composables/pluto-registry.ts +1 -0
- package/app/composables/pluto-settings.ts +16 -2
- package/app/layouts/admin.vue +2 -1
- package/app/pages/admin/content/[type]/[id].vue +3 -1
- package/app/pages/admin/content/[type]/index.vue +2 -1
- package/app/pages/admin/content/[type]/new.vue +2 -1
- package/app/pages/admin/home.vue +6 -4
- package/app/pages/admin/settings.vue +10 -5
- package/app/plugins/pluto-core-registrations.ts +10 -7
- package/app/plugins/pluto-i18n.ts +20 -0
- package/i18n/locales/en.json +137 -0
- package/i18n/locales/pt-BR.json +137 -0
- package/modules/pluto-i18n/index.ts +89 -0
- package/modules/pluto-i18n/locale-files.ts +187 -0
- package/package.json +1 -1
- package/server/utils/pluto-content-handlers.ts +30 -7
- package/server/utils/pluto-error.ts +27 -0
- package/shared/types/i18n.d.ts +10 -0
- package/shared/types/i18n.ts +14 -0
- package/shared/types/registry.ts +13 -0
- package/shared/utils/content-validate.ts +17 -7
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { MessageParams } from './internal/i18n-core'
|
|
2
|
+
import {
|
|
3
|
+
createTranslator,
|
|
4
|
+
formatDate,
|
|
5
|
+
formatNumber,
|
|
6
|
+
} from './internal/i18n-core'
|
|
7
|
+
import {
|
|
8
|
+
ADMIN_LOCALE_COOKIE,
|
|
9
|
+
detectAdminLocale,
|
|
10
|
+
getConfig,
|
|
11
|
+
loadPlutoLocale,
|
|
12
|
+
messageCache,
|
|
13
|
+
readAdminPreference,
|
|
14
|
+
SITE_LOCALE_COOKIE,
|
|
15
|
+
uiLocaleCache,
|
|
16
|
+
useLocaleCookie,
|
|
17
|
+
} from './internal/i18n-runtime'
|
|
18
|
+
|
|
19
|
+
/** Missing keys already reported. Shared, so a key warns once per process. */
|
|
20
|
+
const warnedKeys = new Set<string>()
|
|
21
|
+
|
|
22
|
+
function warnOnce(message: string) {
|
|
23
|
+
if (import.meta.dev && !warnedKeys.has(message)) {
|
|
24
|
+
warnedKeys.add(message)
|
|
25
|
+
console.warn(message)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The state both scopes share: the active locale, the translator, the
|
|
31
|
+
* formatters, and the Nuxt UI locale object. The admin and the site keep a
|
|
32
|
+
* separate active locale and a shared message cache.
|
|
33
|
+
*/
|
|
34
|
+
function useLocaleScope(scope: 'admin' | 'site') {
|
|
35
|
+
const config = getConfig()
|
|
36
|
+
const state = useState<string>(`pluto-${scope}-locale`, () => config.defaultLocale)
|
|
37
|
+
const locales = scope === 'admin' ? config.adminLocales : config.siteLocales
|
|
38
|
+
|
|
39
|
+
const { t, te, tx } = createTranslator({
|
|
40
|
+
locale: () => state.value,
|
|
41
|
+
fallbackLocale: 'en',
|
|
42
|
+
messages: (code) => messageCache.get(code),
|
|
43
|
+
warn: warnOnce,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
state,
|
|
48
|
+
locales,
|
|
49
|
+
locale: computed(() => state.value),
|
|
50
|
+
uiLocale: computed(() => uiLocaleCache.get(state.value) ?? uiLocaleCache.get('en')),
|
|
51
|
+
t: (key: string, params?: MessageParams | number) => t(key, params),
|
|
52
|
+
tx: (value: string, params?: MessageParams | number) => tx(value, params),
|
|
53
|
+
te,
|
|
54
|
+
n: (value: number, options?: Intl.NumberFormatOptions) => formatNumber(state.value, value, options),
|
|
55
|
+
d: (value: Date | number | string, options?: Intl.DateTimeFormatOptions) => formatDate(state.value, value, options),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Translation for the admin. The admin locale is per user: a stored choice
|
|
61
|
+
* (cookie, and the registered locale driver), else the browser language, else
|
|
62
|
+
* the default locale.
|
|
63
|
+
*
|
|
64
|
+
* `tx(value)` translates a key-or-literal string. It translates only when the
|
|
65
|
+
* value looks like a key and the key exists. Any other value returns as it is.
|
|
66
|
+
*/
|
|
67
|
+
export function usePlutoI18n() {
|
|
68
|
+
const scope = useLocaleScope('admin')
|
|
69
|
+
const registry = usePlutoRegistry()
|
|
70
|
+
const cookie = useLocaleCookie(ADMIN_LOCALE_COOKIE)
|
|
71
|
+
const driver = computed(() => registry.localeDrivers.list.value.at(-1) ?? null)
|
|
72
|
+
|
|
73
|
+
/** The stored choice. Null means Automatic. */
|
|
74
|
+
const preference = useState<string | null>('pluto-admin-locale-preference', () => readAdminPreference())
|
|
75
|
+
|
|
76
|
+
async function switchTo(code: string) {
|
|
77
|
+
await loadPlutoLocale(code)
|
|
78
|
+
scope.state.value = code
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Switches the admin language at once. A code is an explicit choice. Null is
|
|
83
|
+
* Automatic: the browser language is detected again. The cookie keeps the
|
|
84
|
+
* explicit code, or is cleared for Automatic. Then the registered locale
|
|
85
|
+
* driver, if any, saves the choice. A driver error rejects the promise, and
|
|
86
|
+
* the switch stays.
|
|
87
|
+
*/
|
|
88
|
+
async function setLocale(code: string | null) {
|
|
89
|
+
if (code !== null && !scope.locales.includes(code)) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
await switchTo(code ?? detectAdminLocale())
|
|
93
|
+
preference.value = code
|
|
94
|
+
cookie.value = code
|
|
95
|
+
await driver.value?.save(code)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reads the user's stored language from the locale driver. A stored code
|
|
100
|
+
* switches the admin to it and sets the cookie. No stored code means
|
|
101
|
+
* Automatic: the cookie is cleared, and the browser language applies. With
|
|
102
|
+
* no driver, or on a driver error, nothing changes.
|
|
103
|
+
*/
|
|
104
|
+
async function syncUserLocale() {
|
|
105
|
+
if (!driver.value) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
let stored: string | null
|
|
109
|
+
try {
|
|
110
|
+
stored = await driver.value.load()
|
|
111
|
+
} catch (error) {
|
|
112
|
+
warnOnce(`[pluto-i18n] Could not load the user's language: ${error instanceof Error ? error.message : String(error)}`)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
if (stored && scope.locales.includes(stored)) {
|
|
116
|
+
await switchTo(stored)
|
|
117
|
+
preference.value = stored
|
|
118
|
+
cookie.value = stored
|
|
119
|
+
} else {
|
|
120
|
+
await switchTo(detectAdminLocale())
|
|
121
|
+
preference.value = null
|
|
122
|
+
cookie.value = null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
locale: scope.locale,
|
|
128
|
+
locales: computed(() => scope.locales),
|
|
129
|
+
uiLocale: scope.uiLocale,
|
|
130
|
+
preference: computed(() => preference.value),
|
|
131
|
+
t: scope.t,
|
|
132
|
+
tx: scope.tx,
|
|
133
|
+
te: scope.te,
|
|
134
|
+
n: scope.n,
|
|
135
|
+
d: scope.d,
|
|
136
|
+
setLocale,
|
|
137
|
+
syncUserLocale,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Translation for the public site. The site locale belongs to the app: the
|
|
143
|
+
* `pluto-site-locale` cookie, else the `plutoI18n.siteLocale` option, else the
|
|
144
|
+
* default locale. It does not follow the admin language.
|
|
145
|
+
*/
|
|
146
|
+
export function usePlutoSiteI18n() {
|
|
147
|
+
const scope = useLocaleScope('site')
|
|
148
|
+
const cookie = useLocaleCookie(SITE_LOCALE_COOKIE)
|
|
149
|
+
|
|
150
|
+
/** Switches the site language, and keeps it in the `pluto-site-locale` cookie. */
|
|
151
|
+
async function setLocale(code: string) {
|
|
152
|
+
if (!scope.locales.includes(code)) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
await loadPlutoLocale(code)
|
|
156
|
+
scope.state.value = code
|
|
157
|
+
cookie.value = code
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
locale: scope.locale,
|
|
162
|
+
locales: computed(() => scope.locales),
|
|
163
|
+
uiLocale: scope.uiLocale,
|
|
164
|
+
t: scope.t,
|
|
165
|
+
tx: scope.tx,
|
|
166
|
+
te: scope.te,
|
|
167
|
+
n: scope.n,
|
|
168
|
+
d: scope.d,
|
|
169
|
+
setLocale,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Capabilities declared by every layer, in registry order. The label and the
|
|
3
|
+
* description are key-or-literal. They resolve with `tx()` inside the
|
|
4
|
+
* `computed`, so a locale switch updates them.
|
|
5
|
+
*/
|
|
2
6
|
export function usePlutoCapabilities() {
|
|
3
7
|
const registry = usePlutoRegistry()
|
|
4
|
-
|
|
8
|
+
const { tx } = usePlutoI18n()
|
|
9
|
+
|
|
10
|
+
const items = computed<PlutoCapability[]>(() =>
|
|
11
|
+
registry.capabilities.list.value.map((capability) => ({
|
|
12
|
+
...capability,
|
|
13
|
+
label: tx(capability.label),
|
|
14
|
+
description: capability.description === undefined ? undefined : tx(capability.description),
|
|
15
|
+
}))
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return { items }
|
|
5
19
|
}
|
|
6
20
|
|
|
7
21
|
/**
|
|
@@ -20,6 +20,7 @@ export function usePlutoRegistry(): PlutoRegistry {
|
|
|
20
20
|
pages: createOwnedRegistry<PlutoAdminPage>(),
|
|
21
21
|
settingsPanels: createOwnedRegistry<PlutoSettingsPanel>(),
|
|
22
22
|
settingsDrivers: createOwnedRegistry<PlutoSettingsDriver>(),
|
|
23
|
+
localeDrivers: createOwnedRegistry<PlutoLocaleDriver>(),
|
|
23
24
|
dashboardWidgets: createOwnedRegistry<PlutoDashboardWidget>(),
|
|
24
25
|
capabilities: createOwnedRegistry<PlutoCapability>(),
|
|
25
26
|
permissionsDrivers: createOwnedRegistry<PlutoPermissionsDriver>(),
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Settings panels registered by every layer, in registry order. The title and
|
|
3
|
+
* the description are key-or-literal. They resolve with `tx()` inside the
|
|
4
|
+
* `computed`, so a locale switch updates them.
|
|
5
|
+
*/
|
|
2
6
|
export function usePlutoSettingsPanels() {
|
|
3
7
|
const registry = usePlutoRegistry()
|
|
4
|
-
|
|
8
|
+
const { tx } = usePlutoI18n()
|
|
9
|
+
|
|
10
|
+
const items = computed<PlutoSettingsPanel[]>(() =>
|
|
11
|
+
registry.settingsPanels.list.value.map((panel) => ({
|
|
12
|
+
...panel,
|
|
13
|
+
title: tx(panel.title),
|
|
14
|
+
description: panel.description === undefined ? undefined : tx(panel.description),
|
|
15
|
+
}))
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return { items }
|
|
5
19
|
}
|
|
6
20
|
|
|
7
21
|
/**
|
package/app/layouts/admin.vue
CHANGED
|
@@ -34,6 +34,7 @@ const menu = computed<SidebarMenuItem[]>(() => navItems.value.map(toMenuItem))
|
|
|
34
34
|
const isSidebarOpen = useState<boolean>('pluto-admin-sidebar-open', () => false)
|
|
35
35
|
|
|
36
36
|
const commandGroups = usePlutoCommandPalette()
|
|
37
|
+
const { t } = usePlutoI18n()
|
|
37
38
|
|
|
38
39
|
// Keep the consumer app's public-site theme out of the admin.
|
|
39
40
|
usePlutoAdminTheme()
|
|
@@ -95,6 +96,6 @@ const groupStyle = { top: 'var(--ui-header-height)' }
|
|
|
95
96
|
</UDashboardPanel>
|
|
96
97
|
|
|
97
98
|
<!-- Cmd/Ctrl+K. See usePlutoCommandPalette for the commands. -->
|
|
98
|
-
<UDashboardSearch :groups="commandGroups" placeholder="
|
|
99
|
+
<UDashboardSearch :groups="commandGroups" :placeholder="t('pluto.palette.placeholder')" />
|
|
99
100
|
</UDashboardGroup>
|
|
100
101
|
</template>
|
|
@@ -3,14 +3,16 @@ const route = useRoute()
|
|
|
3
3
|
const typeName = route.params.type as string
|
|
4
4
|
|
|
5
5
|
const { byName } = usePlutoContentTypes()
|
|
6
|
+
const { t } = usePlutoI18n()
|
|
6
7
|
const type = byName(typeName)
|
|
8
|
+
const text = usePlutoContentText(type)
|
|
7
9
|
|
|
8
10
|
if (!type) {
|
|
9
11
|
throw createError({ statusCode: 404, statusMessage: 'Content type not found.', fatal: true })
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
useHead({
|
|
13
|
-
title:
|
|
15
|
+
title: () => t('pluto.content.edit', { label: text.label }),
|
|
14
16
|
})
|
|
15
17
|
</script>
|
|
16
18
|
|
|
@@ -4,13 +4,14 @@ const typeName = route.params.type as string
|
|
|
4
4
|
|
|
5
5
|
const { byName } = usePlutoContentTypes()
|
|
6
6
|
const type = byName(typeName)
|
|
7
|
+
const text = usePlutoContentText(type)
|
|
7
8
|
|
|
8
9
|
if (!type) {
|
|
9
10
|
throw createError({ statusCode: 404, statusMessage: 'Content type not found.', fatal: true })
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
useHead({
|
|
13
|
-
title:
|
|
14
|
+
title: () => text.labelPlural,
|
|
14
15
|
})
|
|
15
16
|
</script>
|
|
16
17
|
|
|
@@ -4,13 +4,14 @@ const typeName = route.params.type as string
|
|
|
4
4
|
|
|
5
5
|
const { byName } = usePlutoContentTypes()
|
|
6
6
|
const type = byName(typeName)
|
|
7
|
+
const text = usePlutoContentText(type)
|
|
7
8
|
|
|
8
9
|
if (!type) {
|
|
9
10
|
throw createError({ statusCode: 404, statusMessage: 'Content type not found.', fatal: true })
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
useHead({
|
|
13
|
-
title:
|
|
14
|
+
title: () => text.newLabel,
|
|
14
15
|
})
|
|
15
16
|
</script>
|
|
16
17
|
|
package/app/pages/admin/home.vue
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
const { t, d } = usePlutoI18n()
|
|
3
|
+
|
|
2
4
|
useHead({
|
|
3
|
-
title: '
|
|
5
|
+
title: () => t('pluto.pages.dashboard'),
|
|
4
6
|
})
|
|
5
7
|
|
|
6
|
-
const today = new
|
|
8
|
+
const today = computed(() => d(new Date(), { weekday: 'long', month: 'long', day: 'numeric' }))
|
|
7
9
|
|
|
8
10
|
// The header's Create button reuses the command palette's Create commands.
|
|
9
11
|
const groups = usePlutoCommandPalette()
|
|
@@ -21,7 +23,7 @@ function run(item: {
|
|
|
21
23
|
<header class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
22
24
|
<div>
|
|
23
25
|
<p class="text-muted text-sm">{{ today }}</p>
|
|
24
|
-
<h1 class="text-highlighted text-2xl font-semibold tracking-tight lg:text-3xl">
|
|
26
|
+
<h1 class="text-highlighted text-2xl font-semibold tracking-tight lg:text-3xl">{{ t('pluto.pages.dashboard') }}</h1>
|
|
25
27
|
</div>
|
|
26
28
|
|
|
27
29
|
<UButton
|
|
@@ -33,7 +35,7 @@ function run(item: {
|
|
|
33
35
|
/>
|
|
34
36
|
|
|
35
37
|
<UDropdownMenu v-else-if="createItems.length > 1" :items="createItems" :content="{ align: 'end' }">
|
|
36
|
-
<UButton label="
|
|
38
|
+
<UButton :label="t('pluto.common.create')" icon="lucide:plus" trailing-icon="lucide:chevron-down" size="md" />
|
|
37
39
|
</UDropdownMenu>
|
|
38
40
|
</header>
|
|
39
41
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
const { t } = usePlutoI18n()
|
|
3
|
+
|
|
2
4
|
useHead({
|
|
3
|
-
title: '
|
|
5
|
+
title: () => t('pluto.pages.settings'),
|
|
4
6
|
})
|
|
5
7
|
|
|
6
8
|
const { items } = usePlutoSettingsPanels()
|
|
@@ -16,13 +18,16 @@ onMounted(() => {
|
|
|
16
18
|
<template>
|
|
17
19
|
<main>
|
|
18
20
|
<AdminView>
|
|
19
|
-
<h1 class="text-3xl font-bold lg:text-4xl">
|
|
21
|
+
<h1 class="text-3xl font-bold lg:text-4xl">{{ t('pluto.pages.settings') }}</h1>
|
|
22
|
+
|
|
23
|
+
<!-- Per-user, so it needs no capability and no settings driver. -->
|
|
24
|
+
<PlutoSettingsLanguage />
|
|
20
25
|
|
|
21
26
|
<UAlert
|
|
22
27
|
v-if="!hasDriver"
|
|
28
|
+
:title="t('pluto.settings.noStorage')"
|
|
29
|
+
:description="t('pluto.settings.noStorageHint')"
|
|
23
30
|
color="warning"
|
|
24
|
-
title="No settings storage"
|
|
25
|
-
description="No installed layer provides settings storage yet."
|
|
26
31
|
/>
|
|
27
32
|
|
|
28
33
|
<template v-else>
|
|
@@ -37,7 +42,7 @@ onMounted(() => {
|
|
|
37
42
|
|
|
38
43
|
<div class="flex">
|
|
39
44
|
<UButton :loading="status === 'saving'" icon="lucide:save" @click="save">
|
|
40
|
-
|
|
45
|
+
{{ t('pluto.common.save') }}
|
|
41
46
|
</UButton>
|
|
42
47
|
</div>
|
|
43
48
|
</template>
|
|
@@ -14,6 +14,9 @@ import PlutoDashboardQuickActions from '../components/Pluto/Dashboard/PlutoDashb
|
|
|
14
14
|
// Home/Settings nav entries, so every other layer's nav item sorts between
|
|
15
15
|
// them by default (default order is 100).
|
|
16
16
|
//
|
|
17
|
+
// Labels and titles are translation keys, not English text. The consumer
|
|
18
|
+
// composables resolve them with `tx()`. See the i18n skill.
|
|
19
|
+
//
|
|
17
20
|
// Everything core itself contributes to the registry lives in this one
|
|
18
21
|
// `definePlutoExtension` call — `definePlutoExtension` replaces a layer's
|
|
19
22
|
// whole contribution on every call, not just the buckets that call passes,
|
|
@@ -27,12 +30,12 @@ export default defineNuxtPlugin(() => {
|
|
|
27
30
|
definePlutoExtension({
|
|
28
31
|
id: 'pluto',
|
|
29
32
|
nav: [
|
|
30
|
-
{ id: 'home', order: 0, label: '
|
|
31
|
-
{ id: 'settings', order: 1000, label: '
|
|
33
|
+
{ id: 'home', order: 0, label: 'pluto.nav.home', icon: 'lucide:house', to: '/admin/home' },
|
|
34
|
+
{ id: 'settings', order: 1000, label: 'pluto.nav.settings', icon: 'lucide:settings', to: '/admin/settings' },
|
|
32
35
|
],
|
|
33
36
|
pages: [
|
|
34
|
-
{ id: 'home', path: '/admin/home', title: '
|
|
35
|
-
{ id: 'settings', path: '/admin/settings', title: '
|
|
37
|
+
{ id: 'home', path: '/admin/home', title: 'pluto.pages.dashboard', icon: 'lucide:house' },
|
|
38
|
+
{ id: 'settings', path: '/admin/settings', title: 'pluto.pages.settings', icon: 'lucide:settings' },
|
|
36
39
|
],
|
|
37
40
|
contentFieldWidgets: [
|
|
38
41
|
{ id: 'text', fieldType: 'text', component: PlutoFieldText },
|
|
@@ -52,7 +55,7 @@ export default defineNuxtPlugin(() => {
|
|
|
52
55
|
{
|
|
53
56
|
id: 'content-overview',
|
|
54
57
|
order: 0,
|
|
55
|
-
title: '
|
|
58
|
+
title: 'pluto.dashboard.overview.title',
|
|
56
59
|
colSpan: 3,
|
|
57
60
|
component: PlutoDashboardContentOverview,
|
|
58
61
|
enabled: () => usePlutoContentTypes().items.value.length > 0,
|
|
@@ -62,7 +65,7 @@ export default defineNuxtPlugin(() => {
|
|
|
62
65
|
{
|
|
63
66
|
id: 'content-activity',
|
|
64
67
|
order: 10,
|
|
65
|
-
title: '
|
|
68
|
+
title: 'pluto.dashboard.activity.title',
|
|
66
69
|
colSpan: 2,
|
|
67
70
|
component: PlutoDashboardContentActivity,
|
|
68
71
|
enabled: () => usePlutoContentTypes().items.value.length > 0,
|
|
@@ -70,7 +73,7 @@ export default defineNuxtPlugin(() => {
|
|
|
70
73
|
{
|
|
71
74
|
id: 'quick-actions',
|
|
72
75
|
order: 20,
|
|
73
|
-
title: '
|
|
76
|
+
title: 'pluto.dashboard.quickActions.title',
|
|
74
77
|
colSpan: 1,
|
|
75
78
|
component: PlutoDashboardQuickActions,
|
|
76
79
|
},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadPlutoLocale,
|
|
3
|
+
resolveAdminLocale,
|
|
4
|
+
resolveSiteLocale,
|
|
5
|
+
} from '../composables/internal/i18n-runtime'
|
|
6
|
+
|
|
7
|
+
// Resolves the admin locale and the site locale, and loads their messages and
|
|
8
|
+
// Nuxt UI locale before the first render. The locales live in `useState`, so
|
|
9
|
+
// the client reuses the value the server chose: no mismatch on hydration. The
|
|
10
|
+
// resolve runs only when the state is empty (the server, or a client-only
|
|
11
|
+
// route such as /admin/**).
|
|
12
|
+
export default defineNuxtPlugin({
|
|
13
|
+
name: 'pluto-i18n',
|
|
14
|
+
async setup() {
|
|
15
|
+
const admin = useState<string>('pluto-admin-locale', resolveAdminLocale)
|
|
16
|
+
const site = useState<string>('pluto-site-locale', resolveSiteLocale)
|
|
17
|
+
|
|
18
|
+
await Promise.all([loadPlutoLocale(admin.value), loadPlutoLocale(site.value)])
|
|
19
|
+
},
|
|
20
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluto": {
|
|
3
|
+
"app": {
|
|
4
|
+
"name": "Pluto Admin"
|
|
5
|
+
},
|
|
6
|
+
"common": {
|
|
7
|
+
"save": "Save",
|
|
8
|
+
"cancel": "Cancel",
|
|
9
|
+
"delete": "Delete",
|
|
10
|
+
"tryAgain": "Try again",
|
|
11
|
+
"close": "Close",
|
|
12
|
+
"loading": "Loading...",
|
|
13
|
+
"remove": "Remove",
|
|
14
|
+
"edit": "Edit",
|
|
15
|
+
"refresh": "Refresh",
|
|
16
|
+
"settings": "Settings",
|
|
17
|
+
"create": "Create",
|
|
18
|
+
"error": "Error",
|
|
19
|
+
"switchToLightMode": "Switch to light mode",
|
|
20
|
+
"switchToDarkMode": "Switch to dark mode"
|
|
21
|
+
},
|
|
22
|
+
"nav": {
|
|
23
|
+
"home": "Home",
|
|
24
|
+
"settings": "Settings"
|
|
25
|
+
},
|
|
26
|
+
"pages": {
|
|
27
|
+
"dashboard": "Dashboard",
|
|
28
|
+
"settings": "Settings"
|
|
29
|
+
},
|
|
30
|
+
"palette": {
|
|
31
|
+
"goTo": "Go to",
|
|
32
|
+
"placeholder": "Search pages and actions…"
|
|
33
|
+
},
|
|
34
|
+
"content": {
|
|
35
|
+
"new": "New {label}",
|
|
36
|
+
"edit": "Edit {label}",
|
|
37
|
+
"all": "All {labelPlural}",
|
|
38
|
+
"createNew": "Create new {label}",
|
|
39
|
+
"typeNotFound": "Content type not found",
|
|
40
|
+
"typeNotFoundDescription": "No content type named \"{type}\" is registered.",
|
|
41
|
+
"removeConfirm": "Do you really want to remove this item?"
|
|
42
|
+
},
|
|
43
|
+
"form": {
|
|
44
|
+
"published": "Published",
|
|
45
|
+
"unpublished": "Unpublished",
|
|
46
|
+
"saved": "Saved",
|
|
47
|
+
"updated": "{label} updated successfully.",
|
|
48
|
+
"saveError": "An error occurred while saving.",
|
|
49
|
+
"removeError": "An error occurred while removing.",
|
|
50
|
+
"saveDraft": "Save draft",
|
|
51
|
+
"update": "Update",
|
|
52
|
+
"publish": "Publish",
|
|
53
|
+
"unpublish": "Unpublish",
|
|
54
|
+
"fixErrors": "Please fix the following before saving",
|
|
55
|
+
"removeTitle": "Remove {label}"
|
|
56
|
+
},
|
|
57
|
+
"list": {
|
|
58
|
+
"loading": "Loading {labelPlural}…",
|
|
59
|
+
"loadFailed": "Could not load {labelPlural}",
|
|
60
|
+
"loadFailedHint": "Check your connection and try again.",
|
|
61
|
+
"empty": "No {labelPlural} yet",
|
|
62
|
+
"emptyCanWrite": "Create the first {label} to see it here.",
|
|
63
|
+
"emptyReadOnly": "There is nothing here yet.",
|
|
64
|
+
"refreshFailed": "Could not refresh {labelPlural}",
|
|
65
|
+
"refreshFailedHint": "The list below may be out of date.",
|
|
66
|
+
"emptyTable": "No items yet.",
|
|
67
|
+
"removeTitle": "Remove item",
|
|
68
|
+
"showing": "Showing {loaded} {labelPlural}",
|
|
69
|
+
"showingOf": "Showing {loaded} of {total} {labelPlural}",
|
|
70
|
+
"loadingMore": "Loading more…"
|
|
71
|
+
},
|
|
72
|
+
"fields": {
|
|
73
|
+
"noWidget": "No widget registered for field type \"{type}\".",
|
|
74
|
+
"noOptionsSource": "No options source configured for this reference field."
|
|
75
|
+
},
|
|
76
|
+
"dashboard": {
|
|
77
|
+
"noWidgets": "No dashboard widgets are registered yet. A layer can add one with",
|
|
78
|
+
"overview": {
|
|
79
|
+
"title": "Content",
|
|
80
|
+
"label": "Content overview",
|
|
81
|
+
"loadFailed": "Could not load the content counts",
|
|
82
|
+
"thisWeek": "this week",
|
|
83
|
+
"noneThisWeek": "none new this week",
|
|
84
|
+
"meter": "{published} of {total} {labelPlural} published",
|
|
85
|
+
"published": "published | published | published",
|
|
86
|
+
"drafts": "drafts | draft | drafts"
|
|
87
|
+
},
|
|
88
|
+
"activity": {
|
|
89
|
+
"title": "Content activity",
|
|
90
|
+
"subtitle": "Items created per week, last {weeks} weeks",
|
|
91
|
+
"created": "created | created | created",
|
|
92
|
+
"loadFailed": "Could not load the activity",
|
|
93
|
+
"unavailable": "Activity is not available",
|
|
94
|
+
"unavailableHint": "The content backend cannot count items by creation date yet.",
|
|
95
|
+
"none": "Nothing created in the last {weeks} weeks",
|
|
96
|
+
"noneHint": "New items show up here week by week.",
|
|
97
|
+
"weekOf": "Week of"
|
|
98
|
+
},
|
|
99
|
+
"quickActions": {
|
|
100
|
+
"title": "Quick actions",
|
|
101
|
+
"pressBefore": "Press",
|
|
102
|
+
"pressAfter": "to search everything."
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"settings": {
|
|
106
|
+
"noStorage": "No settings storage",
|
|
107
|
+
"noStorageHint": "No installed layer provides settings storage yet.",
|
|
108
|
+
"language": {
|
|
109
|
+
"cardTitle": "Your preferences",
|
|
110
|
+
"label": "Language",
|
|
111
|
+
"description": "Choose the language of the admin. It applies to your account only.",
|
|
112
|
+
"automatic": "Automatic (browser)",
|
|
113
|
+
"saved": "Language updated",
|
|
114
|
+
"saveFailed": "Could not save your language",
|
|
115
|
+
"saveFailedDescription": "The language changed on this device only. {reason}"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"errors": {
|
|
119
|
+
"unknown": "Something went wrong.",
|
|
120
|
+
"contentTypeNotFound": "Content type \"{name}\" not found.",
|
|
121
|
+
"noContentAdapter": "No content adapter is registered.",
|
|
122
|
+
"notFound": "Not found.",
|
|
123
|
+
"bodyNotObject": "Request body must be a JSON object.",
|
|
124
|
+
"validationFailed": "Validation failed."
|
|
125
|
+
},
|
|
126
|
+
"validation": {
|
|
127
|
+
"required": "\"{label}\" is required.",
|
|
128
|
+
"number": "\"{label}\" must be a number.",
|
|
129
|
+
"integer": "\"{label}\" must be an integer.",
|
|
130
|
+
"min": "\"{label}\" must be at least {min}.",
|
|
131
|
+
"max": "\"{label}\" must be at most {max}.",
|
|
132
|
+
"boolean": "\"{label}\" must be a boolean.",
|
|
133
|
+
"option": "\"{label}\" must be one of the allowed options.",
|
|
134
|
+
"maxLength": "\"{label}\" must be at most {max} characters."
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|