Internationalisation (i18n)
How the library handles strings
@conduction/nextcloud-vue ships its own translation bundles (l10n/en.json, l10n/nl.json) and registers them under the nextcloud-vue namespace at runtime. Every user-facing string inside the library is resolved through t('nextcloud-vue', '...') with an English default, so:
- Components work out of the box in the user's current Nextcloud language (for shipped locales).
- Your app does not need to re-translate library strings.
- Every translatable prop is still overridable — pass a pre-translated string and the library prop wins.
For the shipped translations to take effect, consumers MUST call registerTranslations() once during app bootstrap — see Getting Started. Without it, every library string falls back to English even when the user's Nextcloud language is Dutch.
// main.js
import { registerTranslations } from '@conduction/nextcloud-vue'
registerTranslations()
registerTranslations():
- Reads the current language via
getLanguage()from@nextcloud/l10n. - Picks the matching bundle (primary subtag match, falls back to
en). - Calls
register('nextcloud-vue', bundle)from@nextcloud/l10n.
Setting up i18n for your own app strings
The section above covers the library's own strings. Your app's strings (page titles, custom labels, anything you pass into props) still need your own translation setup.
Nextcloud apps use @nextcloud/l10n for translations. It reads Nextcloud's built-in gettext catalog (.po files compiled to JSON) and provides a t() function.
1. Install the package
npm install @nextcloud/l10n
2. Create translation files
Nextcloud looks for l10n/{locale}.json files in your app root. Create at minimum en.json and nl.json:
l10n/en.json
{
"translations": {
"Clients": "Clients",
"Add Client": "Add Client",
"Delete Client": "Delete Client",
"Are you sure you want to permanently delete \"{name}\"?": "Are you sure you want to permanently delete \"{name}\"?",
"Client successfully deleted.": "Client successfully deleted.",
"No clients found": "No clients found",
"Items per page:": "Items per page:",
"Page {current} of {total}": "Page {current} of {total}"
},
"plurals": ""
}
l10n/nl.json
{
"translations": {
"Clients": "Klanten",
"Add Client": "Klant toevoegen",
"Delete Client": "Klant verwijderen",
"Are you sure you want to permanently delete \"{name}\"?": "Weet je zeker dat je \"{name}\" permanent wilt verwijderen?",
"Client successfully deleted.": "Klant succesvol verwijderd.",
"No clients found": "Geen klanten gevonden",
"Items per page:": "Items per pagina:",
"Page {current} of {total}": "Pagina {current} van {total}"
},
"plurals": ""
}
Nextcloud automatically selects the correct file based on the user's language preference.
3. Import the translate function
// In each Vue component or in main.js
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
For Vue 2 components, register t globally so all templates can use it:
// main.js
import Vue from 'vue'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
Vue.prototype.t = t
Vue.prototype.n = n
Overriding a library string
The shipped bundles cover the default labels. To change wording for a specific prop in your app — say a domain-specific verb instead of "Delete" — pass a pre-translated prop and it will override the library's default:
<template>
<CnIndexPage
:title="t('myapp', 'Clients')"
:description="t('myapp', 'Manage your client relationships')"
:empty-text="t('myapp', 'No clients found')"
:add-label="t('myapp', 'Add Client')"
@create="onCreate"
@delete="onDelete">
<!-- Translate the delete dialog -->
<template #delete-dialog="{ item, close }">
<CnDeleteDialog
:dialog-title="t('myapp', 'Delete Client')"
:warning-text="t('myapp', 'Are you sure you want to permanently delete \"{name}\"?').replace('{name}', item.name)"
:success-text="t('myapp', 'Client successfully deleted.')"
:cancel-label="t('myapp', 'Cancel')"
:confirm-label="t('myapp', 'Delete')"
:item="item"
@close="close"
@confirm="onDelete(item.id)" />
</template>
</CnIndexPage>
</template>
Recommended pattern: a translation composable
For apps with many components, create a central translation helper so you don't repeat t('myapp', ...) everywhere:
// src/i18n.js
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
const APP_NAME = 'myapp'
export const i18n = {
// Navigation
clients: () => t(APP_NAME, 'Clients'),
contacts: () => t(APP_NAME, 'Contacts'),
// Actions
add: (entity) => t(APP_NAME, 'Add {entity}').replace('{entity}', entity),
delete: (entity) => t(APP_NAME, 'Delete {entity}').replace('{entity}', entity),
edit: (entity) => t(APP_NAME, 'Edit {entity}').replace('{entity}', entity),
cancel: () => t(APP_NAME, 'Cancel'),
save: () => t(APP_NAME, 'Save'),
// Confirmations
deleteWarning: (name) =>
t(APP_NAME, 'Are you sure you want to permanently delete "{name}"? This action cannot be undone.')
.replace('{name}', name),
deleteSuccess: (entity) => t(APP_NAME, '{entity} successfully deleted.').replace('{entity}', entity),
// Table
noItems: (entity) => t(APP_NAME, 'No {entity} found').replace('{entity}', entity),
itemsPerPage: () => t(APP_NAME, 'Items per page:'),
pageInfo: (current, total) =>
t(APP_NAME, 'Page {current} of {total}')
.replace('{current}', current)
.replace('{total}', total),
}
Then in your components:
<script>
import { i18n } from '../../i18n.js'
export default {
computed: {
strings() {
return {
title: i18n.clients(),
addLabel: i18n.add('Client'),
emptyText: i18n.noItems('clients'),
}
},
},
}
</script>
Adding a new language
- Add
l10n/<lang>.jsonin this repo using the standard Nextcloud format ({"translations": { ... }, "plurals": "" }). - Import the new bundle and add it to the
BUNDLESmap insrc/l10n/index.js. - Run
npm run build— the bundle is inlined intodist/.
Complete props reference by component
Every library string is backed by one of the props below. The shipped bundles cover the default values; override a prop if you need different wording (see "Overriding a library string" above).
CnIndexPage
| Prop | English Default | Notes |
|---|---|---|
title | (required) | Page heading |
description | '' | Subheading |
emptyText | 'No items found' | Empty state message |
addLabel | 'Add {schema.title}' | Add button label |
CnDeleteDialog
| Prop | English Default | Notes |
|---|---|---|
dialogTitle | 'Delete Item' | Dialog heading |
warningText | 'Are you sure…' | Supports {name} placeholder |
successText | 'Item successfully deleted.' | Phase-2 success message |
cancelLabel | 'Cancel' | |
confirmLabel | 'Delete' | |
closeLabel | 'Close' | Phase-2 close button |
CnCopyDialog
| Prop | English Default | Notes |
|---|---|---|
dialogTitle | 'Copy Item' | |
patternLabel | 'Naming pattern' | |
successText | 'Item successfully copied.' | |
cancelLabel | 'Cancel' | |
confirmLabel | 'Copy' | |
closeLabel | 'Close' |
CnFormDialog
| Prop | English Default | Notes |
|---|---|---|
dialogTitle | 'Create {title}' / 'Edit {title}' | Auto-derived from schema |
successText | 'Item saved successfully.' | |
cancelLabel | 'Cancel' | |
confirmLabel | 'Create' / 'Save' | Auto-derived from mode |
closeLabel | 'Close' |
CnMassDeleteDialog / CnMassCopyDialog
| Prop | English Default |
|---|---|
cancelLabel | 'Cancel' |
confirmLabel | 'Delete' / 'Copy' |
closeLabel | 'Close' |
removeLabel | 'Remove from list' |
CnMassExportDialog
| Prop | English Default |
|---|---|
formatLabel | 'Export format' |
cancelLabel | 'Cancel' |
confirmLabel | 'Export' |
closeLabel | 'Close' |
CnMassImportDialog
| Prop | English Default |
|---|---|
supportedFormatsLabel | 'Supported file types:' |
selectFileLabel | 'Select File' |
sheetLabel | 'Sheet' |
foundLabel | 'Found' |
createdLabel | 'Created' |
cancelLabel | 'Cancel' |
confirmLabel | 'Import' |
closeLabel | 'Close' |
CnMassActionBar
| Prop | English Default | Notes |
|---|---|---|
menuLabelTemplate | 'Mass Actions ({count})' | {count} is replaced |
importLabel | 'Import' | |
exportLabel | 'Export' | |
copyLabel | 'Copy' | |
deleteLabel | 'Delete' |
CnPagination
| Prop | English Default | Notes |
|---|---|---|
firstLabel | 'First' | |
previousLabel | 'Previous' | |
nextLabel | 'Next' | |
lastLabel | 'Last' | |
itemsPerPageLabel | 'Items per page:' | |
pageInfoFormat | 'Page {current} of {total}' | {current} and {total} replaced |
CnDataTable
| Prop | English Default |
|---|---|
emptyText | 'No items found' |
loadingText | 'Loading...' |
CnFilterBar / CnIndexSidebar
| Prop | English Default |
|---|---|
searchPlaceholder | 'Search...' / 'Type to search...' |
clearAllLabel | 'Clear filters' |
searchTabLabel | 'Search' |
columnsTabLabel | 'Columns' |
searchLabel | 'Search' |
filtersLabel | 'Filters' |
columnsHeading | 'Column Visibility' |
columnsDescription | 'Select which columns to display in the table' |
Schema-driven labels
Column headers and form field labels are auto-generated from the schema. Override per-field via:
// columnOverrides — override table column labels
columnOverrides: {
clientType: { label: t('myapp', 'Client Type') },
createdAt: { label: t('myapp', 'Date Added') },
}
// fieldOverrides — override form field labels
fieldOverrides: {
clientType: { label: t('myapp', 'Client Type'), placeholder: t('myapp', 'Select a type…') },
}
Row action labels come from your actions array:
actions: [
{ label: t('myapp', 'Edit'), icon: 'Pencil', handler: 'edit' },
{ label: t('myapp', 'Delete'), icon: 'Delete', handler: 'delete', destructive: true },
]