@plutocms/pluto 0.12.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 +24 -0
- package/FEATURES.md +7 -0
- package/app/app.vue +5 -2
- package/app/assets/css/admin-theme.css +16 -7
- 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 +84 -42
- 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/Navbar/PlutoNavbarAdmin.vue +4 -1
- 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/Pluto/View/PlutoViewAdminPanel.vue +9 -1
- package/app/components/Pluto/View/PlutoViewAside.vue +14 -7
- package/app/components/modal/Modal.vue +1 -1
- 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-list.ts +88 -7
- 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/scripts/generate-admin-theme.cjs +21 -7
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluto": {
|
|
3
|
+
"app": {
|
|
4
|
+
"name": "Pluto Admin"
|
|
5
|
+
},
|
|
6
|
+
"common": {
|
|
7
|
+
"save": "Salvar",
|
|
8
|
+
"cancel": "Cancelar",
|
|
9
|
+
"delete": "Excluir",
|
|
10
|
+
"tryAgain": "Tentar novamente",
|
|
11
|
+
"close": "Fechar",
|
|
12
|
+
"loading": "Carregando...",
|
|
13
|
+
"remove": "Remover",
|
|
14
|
+
"edit": "Editar",
|
|
15
|
+
"refresh": "Atualizar",
|
|
16
|
+
"settings": "Configurações",
|
|
17
|
+
"create": "Criar",
|
|
18
|
+
"error": "Erro",
|
|
19
|
+
"switchToLightMode": "Mudar para o tema claro",
|
|
20
|
+
"switchToDarkMode": "Mudar para o tema escuro"
|
|
21
|
+
},
|
|
22
|
+
"nav": {
|
|
23
|
+
"home": "Início",
|
|
24
|
+
"settings": "Configurações"
|
|
25
|
+
},
|
|
26
|
+
"pages": {
|
|
27
|
+
"dashboard": "Painel",
|
|
28
|
+
"settings": "Configurações"
|
|
29
|
+
},
|
|
30
|
+
"palette": {
|
|
31
|
+
"goTo": "Ir para",
|
|
32
|
+
"placeholder": "Buscar páginas e ações…"
|
|
33
|
+
},
|
|
34
|
+
"content": {
|
|
35
|
+
"new": "Criar {label}",
|
|
36
|
+
"edit": "Editar {label}",
|
|
37
|
+
"all": "Ver {labelPlural}",
|
|
38
|
+
"createNew": "Criar {label}",
|
|
39
|
+
"typeNotFound": "Tipo de conteúdo não encontrado",
|
|
40
|
+
"typeNotFoundDescription": "Nenhum tipo de conteúdo chamado \"{type}\" está registrado.",
|
|
41
|
+
"removeConfirm": "Deseja mesmo remover este item?"
|
|
42
|
+
},
|
|
43
|
+
"form": {
|
|
44
|
+
"published": "Publicado",
|
|
45
|
+
"unpublished": "Não publicado",
|
|
46
|
+
"saved": "Salvo",
|
|
47
|
+
"updated": "{label}: alterações salvas.",
|
|
48
|
+
"saveError": "Ocorreu um erro ao salvar.",
|
|
49
|
+
"removeError": "Ocorreu um erro ao remover.",
|
|
50
|
+
"saveDraft": "Salvar rascunho",
|
|
51
|
+
"update": "Atualizar",
|
|
52
|
+
"publish": "Publicar",
|
|
53
|
+
"unpublish": "Despublicar",
|
|
54
|
+
"fixErrors": "Corrija os itens abaixo antes de salvar",
|
|
55
|
+
"removeTitle": "Remover {label}"
|
|
56
|
+
},
|
|
57
|
+
"list": {
|
|
58
|
+
"loading": "Carregando {labelPlural}…",
|
|
59
|
+
"loadFailed": "Não foi possível carregar {labelPlural}",
|
|
60
|
+
"loadFailedHint": "Verifique a conexão e tente novamente.",
|
|
61
|
+
"empty": "Ainda não há {labelPlural}",
|
|
62
|
+
"emptyCanWrite": "Use “Criar {label}” para adicionar o primeiro item.",
|
|
63
|
+
"emptyReadOnly": "Ainda não há nada aqui.",
|
|
64
|
+
"refreshFailed": "Não foi possível atualizar {labelPlural}",
|
|
65
|
+
"refreshFailedHint": "A lista abaixo pode estar desatualizada.",
|
|
66
|
+
"emptyTable": "Nenhum item ainda.",
|
|
67
|
+
"removeTitle": "Remover item",
|
|
68
|
+
"showing": "Mostrando {loaded} {labelPlural}",
|
|
69
|
+
"showingOf": "Mostrando {loaded} de {total} {labelPlural}",
|
|
70
|
+
"loadingMore": "Carregando mais…"
|
|
71
|
+
},
|
|
72
|
+
"fields": {
|
|
73
|
+
"noWidget": "Nenhum widget registrado para o tipo de campo \"{type}\".",
|
|
74
|
+
"noOptionsSource": "Nenhuma fonte de opções configurada para este campo de referência."
|
|
75
|
+
},
|
|
76
|
+
"dashboard": {
|
|
77
|
+
"noWidgets": "Ainda não há widgets registrados no painel. Uma camada pode adicionar um com",
|
|
78
|
+
"overview": {
|
|
79
|
+
"title": "Conteúdo",
|
|
80
|
+
"label": "Visão geral do conteúdo",
|
|
81
|
+
"loadFailed": "Não foi possível carregar as contagens de conteúdo",
|
|
82
|
+
"thisWeek": "esta semana",
|
|
83
|
+
"noneThisWeek": "nada novo esta semana",
|
|
84
|
+
"meter": "Publicação: {published} de {total} {labelPlural}",
|
|
85
|
+
"published": "publicados | publicado | publicados",
|
|
86
|
+
"drafts": "rascunhos | rascunho | rascunhos"
|
|
87
|
+
},
|
|
88
|
+
"activity": {
|
|
89
|
+
"title": "Atividade de conteúdo",
|
|
90
|
+
"subtitle": "Itens criados por semana, últimas {weeks} semanas",
|
|
91
|
+
"created": "criados | criado | criados",
|
|
92
|
+
"loadFailed": "Não foi possível carregar a atividade",
|
|
93
|
+
"unavailable": "Atividade indisponível",
|
|
94
|
+
"unavailableHint": "O backend de conteúdo ainda não consegue contar itens pela data de criação.",
|
|
95
|
+
"none": "Nada criado nas últimas {weeks} semanas",
|
|
96
|
+
"noneHint": "Os novos itens aparecem aqui semana a semana.",
|
|
97
|
+
"weekOf": "Semana de"
|
|
98
|
+
},
|
|
99
|
+
"quickActions": {
|
|
100
|
+
"title": "Ações rápidas",
|
|
101
|
+
"pressBefore": "Pressione",
|
|
102
|
+
"pressAfter": "para buscar tudo."
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"settings": {
|
|
106
|
+
"noStorage": "Sem armazenamento de configurações",
|
|
107
|
+
"noStorageHint": "Nenhuma camada instalada oferece armazenamento de configurações ainda.",
|
|
108
|
+
"language": {
|
|
109
|
+
"cardTitle": "Suas preferências",
|
|
110
|
+
"label": "Idioma",
|
|
111
|
+
"description": "Escolha o idioma do painel. Vale só para a sua conta.",
|
|
112
|
+
"automatic": "Automático (navegador)",
|
|
113
|
+
"saved": "Idioma atualizado",
|
|
114
|
+
"saveFailed": "Não foi possível salvar o idioma",
|
|
115
|
+
"saveFailedDescription": "O idioma mudou apenas neste dispositivo. {reason}"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"errors": {
|
|
119
|
+
"unknown": "Algo deu errado.",
|
|
120
|
+
"contentTypeNotFound": "Tipo de conteúdo \"{name}\" não encontrado.",
|
|
121
|
+
"noContentAdapter": "Nenhum adaptador de conteúdo está registrado.",
|
|
122
|
+
"notFound": "Não encontrado.",
|
|
123
|
+
"bodyNotObject": "O corpo da requisição deve ser um objeto JSON.",
|
|
124
|
+
"validationFailed": "Falha na validação."
|
|
125
|
+
},
|
|
126
|
+
"validation": {
|
|
127
|
+
"required": "Preencha \"{label}\".",
|
|
128
|
+
"number": "Informe um número em \"{label}\".",
|
|
129
|
+
"integer": "Informe um número inteiro em \"{label}\".",
|
|
130
|
+
"min": "Informe em \"{label}\" um valor de no mínimo {min}.",
|
|
131
|
+
"max": "Informe em \"{label}\" um valor de no máximo {max}.",
|
|
132
|
+
"boolean": "Use apenas verdadeiro ou falso em \"{label}\".",
|
|
133
|
+
"option": "Escolha uma das opções permitidas em \"{label}\".",
|
|
134
|
+
"maxLength": "Use no máximo {max} caracteres em \"{label}\"."
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { PlutoI18nOptions } from './locale-files'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
import { addTemplate, defineNuxtModule, getLayerDirectories, resolvePath, updateTemplates, useLogger } from '@nuxt/kit'
|
|
5
|
+
import {
|
|
6
|
+
findLayerLocaleFiles,
|
|
7
|
+
LOCALE_DIR,
|
|
8
|
+
renderI18nTemplate,
|
|
9
|
+
renderI18nTypes,
|
|
10
|
+
resolveI18nOptions,
|
|
11
|
+
} from './locale-files'
|
|
12
|
+
|
|
13
|
+
export type { PlutoI18nOptions } from './locale-files'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Finds every layer's `i18n/locales/<code>.json` and generates
|
|
17
|
+
* `#build/pluto-i18n.mjs`: the locale codes and one lazy loader per locale.
|
|
18
|
+
* The files of all layers merge, base layer first and the app last, so the
|
|
19
|
+
* app overrides a layer's text. Adding a language is adding the JSON files.
|
|
20
|
+
* No code change.
|
|
21
|
+
*
|
|
22
|
+
* The module also puts the resolved options in
|
|
23
|
+
* `runtimeConfig.public.plutoI18n`. See the i18n skill.
|
|
24
|
+
*/
|
|
25
|
+
export default defineNuxtModule<PlutoI18nOptions>({
|
|
26
|
+
meta: { name: 'pluto-i18n', configKey: 'plutoI18n' },
|
|
27
|
+
defaults: {},
|
|
28
|
+
async setup(options, nuxt) {
|
|
29
|
+
const logger = useLogger('pluto-i18n')
|
|
30
|
+
const layerRoots = getLayerDirectories(nuxt).map((layer) => layer.root)
|
|
31
|
+
|
|
32
|
+
function listDir(dir: string): string[] {
|
|
33
|
+
return existsSync(dir) ? readdirSync(dir) : []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Nuxt UI's locale files. The generated file imports them by absolute
|
|
37
|
+
// path, so the app needs no direct `@nuxt/ui` dependency.
|
|
38
|
+
const uiLocaleDir = dirname(await resolvePath('@nuxt/ui/runtime/locale/en.js'))
|
|
39
|
+
const uiLocaleFiles = listDir(uiLocaleDir)
|
|
40
|
+
.filter((name) => name.endsWith('.js'))
|
|
41
|
+
.map((name) => name.slice(0, -'.js'.length))
|
|
42
|
+
.filter((name) => name !== 'index')
|
|
43
|
+
|
|
44
|
+
let files = findLayerLocaleFiles(layerRoots, listDir)
|
|
45
|
+
const resolved = resolveI18nOptions(options, Object.keys(files))
|
|
46
|
+
|
|
47
|
+
if (!nuxt.options._prepare) {
|
|
48
|
+
for (const warning of resolved.warnings) {
|
|
49
|
+
logger.warn(warning)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
nuxt.options.runtimeConfig.public.plutoI18n = {
|
|
54
|
+
defaultLocale: resolved.defaultLocale,
|
|
55
|
+
adminLocales: resolved.adminLocales,
|
|
56
|
+
siteLocales: resolved.siteLocales,
|
|
57
|
+
siteLocale: resolved.siteLocale,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
addTemplate({
|
|
61
|
+
filename: 'pluto-i18n.mjs',
|
|
62
|
+
write: true,
|
|
63
|
+
getContents: () => renderI18nTemplate({
|
|
64
|
+
files,
|
|
65
|
+
defaultLocale: resolved.defaultLocale,
|
|
66
|
+
uiLocaleDir,
|
|
67
|
+
uiLocaleFiles,
|
|
68
|
+
}),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
addTemplate({
|
|
72
|
+
filename: 'pluto-i18n.d.mts',
|
|
73
|
+
write: true,
|
|
74
|
+
getContents: renderI18nTypes,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// A new or removed locale file changes the generated file. Locale
|
|
78
|
+
// codes and options come from the first run, so restart the dev server
|
|
79
|
+
// after a new language. An edit of an existing file hot-reloads on its own.
|
|
80
|
+
nuxt.hook('builder:watch', async (_event, path) => {
|
|
81
|
+
const normalized = path.replaceAll('\\', '/')
|
|
82
|
+
if (!normalized.includes(`/${LOCALE_DIR}/`) || !normalized.endsWith('.json')) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
files = findLayerLocaleFiles(layerRoots, listDir)
|
|
86
|
+
await updateTemplates({ filter: (template) => template.filename.startsWith('pluto-i18n.') })
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
})
|