Translating the UI
Pivotick writes text of its own: the rail, the menus, the sidebar, the table, the pivot panel, the history, the toasts, and every title tooltip and aria-label behind them. The i18n option puts all of it in your language.
const graph = new Pivotick(container, data, {
i18n: { locale: 'fr' },
})Text you hand to the library is drawn exactly as given and never looked up: node and edge labels, facet and legend labels, pivot names, rail modes, dock tabs, top-bar actions and the rows you add to a context menu. Translate those before passing them in.
Bundled languages
Every build includes complete translations of the built-in UI:
| Language | Locale | Example regional tags |
|---|---|---|
| English (default) | en | en-GB, en-US |
| Simplified Chinese | zh | zh-CN, zh-Hans |
| French | fr | fr-FR, fr-CA |
| German | de | de-DE, de-CH |
| Portuguese | pt | pt-BR, pt-PT |
Set i18n.locale to select one. Regional tags use the same language catalogue while retaining their own number formatting, sorting and plural rules. The Chinese catalogue uses Simplified characters; there is no separate Traditional Chinese translation. A locale without a bundled catalogue uses English unless you supply messages or translate. Omitting locale keeps the existing English default; browser language is not detected.
The catalogues are exported as messagesEn, messagesZh, messagesFr, messagesDe, messagesPt and together as bundledMessages, also available on Pivotick in browser builds.
The option
| Property | Type | Default | Description |
|---|---|---|---|
locale | string | 'en' | A BCP 47 tag such as 'fr', 'de-CH' or 'pt-BR'. It picks the plural rules, how numbers are written and how lists sort. When given, it is also set as the lang of the graph's root. |
messages | Partial<Messages> | {} | Overrides by key. Missing keys use the bundled language, then English. |
translate | (key, params, fallback) => string | undefined | A bridge to your own translation library, asked for every key messages does not have. |
The language is set when the graph is built. To switch it, call graph.destroy() and build the graph again with the new options.
How a message is found
Each piece of text is looked up in this order, and the first answer wins:
- The feature's own option, when it has one:
UI.emptyState.render,UI.busyIndicator.label,UI.simplify.typeLabel, or thetitleandconfirmLabelyou pass toctx.confirm(). These are more specific than any catalogue. messages, your table of keys.translate, your bridge.- The bundled language, selected by
locale. - English, when the language has no bundled catalogue.
Partial overrides are supported: change only the terms your application needs and the rest uses the selected language. Unsupported languages fall through to English without a warning.
Keys and messages
Every message has a stable, dotted key named after where it appears: 'contextMenu.node.pin', 'history.menu.empty', 'sidebar.neighbors.connections'. The keys are typed from the English catalogue, so in TypeScript a misspelt key fails to compile and your editor completes the rest.
A message takes one of three forms.
Text with placeholders
A string, with {name} where a value goes:
messages: {
'search.result.inGroup': 'dans {group}',
'history.menu.count': '{count} sur {limit}',
}A number is written the way the locale writes it before it goes in: 1,234 under 'en', 1.234 under 'de', 1 234 under 'fr'. A string goes in as written.
Plural forms
A message that depends on a count is an object with one entry per plural category the language uses. The category is chosen from the count param by the browser's Intl.PluralRules, so the rules are the ones CLDR defines for the locale. other is required, since every language has it.
// English and French need two forms; French counts zero as one
'count.nodes': { one: '{count} nœud', other: '{count} nœuds' },
// Polish needs four: 1 węzeł, 3 węzły, 5 węzłów, 1,5 węzła
'count.nodes': { one: '{count} węzeł', few: '{count} węzły', many: '{count} węzłów', other: '{count} węzła' },The categories are zero, one, two, few, many and other. A form the language does not use is never picked, and a missing one falls back to other.
Fallback English keeps English plural rules under any locale. For example, an unsupported language that treats zero as singular still gets "0 nodes" from the English fallback.
A function
For anything a template cannot say, a message can be a function of the raw params and the graph's I18n:
'simplify.typeLabel': ({ count, type }, i18n) =>
`${i18n.formatNumber(Number(count))} ${type === 'ip' ? 'adresses IP' : type}`,Whatever its form, a message is text. It is inserted as text, never parsed as HTML, so <b> shows up as written.
Editing PO translation files
The repository’s locales/ directory contains a gettext template, pivotick.pot, and complete zh.po, fr.po, de.po and pt.po files. These files also ship in the npm package. Open a .po file in Poedit or any UTF-8 text editor. Each msgctxt is the stable message key; msgid is the English source and msgstr is the translation. Keep {placeholders} intact. For plural entries, translate every msgstr[n] slot: Chinese has other only, and French, German and Portuguese have one then other. The runtime uses Intl.PluralRules, with many falling back to other, rather than evaluating the gettext header as JavaScript.
From a source checkout with npm ci completed:
# Refresh the template and merge English changes into the PO files
npm run i18n:extract
# Edit locales/*.po, finish empty entries and review any fuzzy translations
# Compile PO files into the TypeScript catalogues used by every build
npm run i18n:compile
# Check completeness, source text, placeholders, plurals and generated files
npm run i18n:check
npm run test:i18nCommit the PO files, template and generated src/i18n/locales/*.ts files together. Do not edit the generated files directly. Extraction preserves existing translations and translator comments, marks changed English text as fuzzy, and leaves new entries empty. Resolve those entries before compiling. Removed keys are dropped from the active catalogue. npm run build runs i18n:check, so an incomplete translation or an outdated generated catalogue fails the build. These commands use existing Node dependencies; no system gettext installation is needed.
To bundle another language, add its locale and plural-slot mapping to scripts/i18n.mjs, extract and translate the new PO file, compile it, then export the generated catalogue from src/i18n/locales/index.ts and add it to bundledMessages. For an application-only language, passing a catalogue as i18n.messages still works without changing Pivotick.
Starting a catalogue from messagesEn
The English catalogue is exported as messagesEn, from the ES build and as Pivotick.messagesEn from the browser builds. It holds every key with its English text, and it is plain data, so it can be written out as the file your translators start from:
import { messagesEn } from 'pivotick'
console.log(JSON.stringify(messagesEn, null, 4))Load the translated file back as messages. In TypeScript, a catalogue declared with satisfies Messages must name every key, so a key added in a later release shows up as a compile error rather than as a stray English string:
import type { Messages } from 'pivotick'
export const messagesFr = {
'count.nodes': { one: '{count} nœud', other: '{count} nœuds' },
// …every other key
} satisfies MessagesUse Partial<Messages> instead to translate as you go.
Bridging your own translation library
If your application already translates through a library, translate keeps one source of truth and one language switch. It is called for every key messages does not have, with:
key: the message key, such as'count.nodes';params: the raw values, with numbers unformatted, so your library's own plural and number rules apply;fallback: the English text, already filled in.
Return a string to use it, or undefined to use the bundled language and then English. Here with i18next, whose count param already drives its plurals:
const graph = new Pivotick(container, data, {
i18n: {
locale: i18next.language,
translate: (key, params, fallback) => {
const found = i18next.exists(`pivotick.${key}`)
return found ? i18next.t(`pivotick.${key}`, params) : undefined
},
},
})Your resource files then write their messages in your library's own syntax ("{{count}} nœuds" in i18next), under the same keys.
A translate that throws is caught: the console says which key, and the bundled language (or English for an unsupported language) is shown. The hook still receives English as its fallback argument, preserving the bridge contract. When messages and translate both have a key, messages wins and translate is not asked.
Numbers, lists, sorting and lang
locale covers more than words. Under it:
- Counts are written with
Intl.NumberFormat: "1,234 nodes" in English, "1.234 Knoten" in German. Fallback English also uses the selected locale’s number formatting. - Lists of names in one sentence are joined with
Intl.ListFormat: "a, b and c", "a, b et c". - Sorts in the table, the neighbours list and the pivot triage pane use
Intl.Collator, so "Émile" sorts beside "Emma", Swedish puts "ä" after "z", and "item 10" comes after "item 9".
When you declare a locale, the graph's root element gets it as lang, and so do the parts the library mounts outside the root (tooltips, dropdowns, suggestion lists). Screen readers then read the interface in the right voice. Without a locale, no lang is set: the library's text is English, but your data inside the graph is in your page's language, and the page's own lang describes it best.
Right-to-left layout is not supported: there is no dir handling, and the interface is laid out left to right in every language.
One language per graph
Each graph has its own language. Two graphs on one page can use two languages, and neither affects the other.
A graph inside a graph speaks its parent's: the subgraph drawn inside a cluster, and the small neighbours graph in the sidebar.
Text the library writes into your data is written once, in the graph's language: a history entry's label ("Hid 3 nodes") and the body a new note starts with. Rebuilding the graph in another language leaves those as they were.
In code: graph.i18n and plugins
graph.i18n is the graph's language, the same object the library writes with:
| Member | What it does |
|---|---|
locale | The tag in use: yours, or 'en'. |
t(key, params?) | The text of a built-in message, through the lookup above. |
formatNumber(value, options?) | A number in the locale's digits. Takes Intl.NumberFormat options. |
formatList(items, options?) | Items joined as a sentence. Takes Intl.ListFormat options. |
compare(a, b) | A comparator in the locale's order, safe to pass to sort as is. |
collator(options?) | The Intl.Collator behind compare, numeric by default. |
A plugin reads the same object as ctx.i18n. Text the plugin writes itself is the plugin's to translate, keyed on ctx.i18n.locale; formatting its counts and sorting its lists through ctx.i18n keeps them consistent with the rest of the graph.
const nodeCounter = {
name: 'node-counter',
install(ctx) {
const words = { fr: 'Total', de: 'Gesamt' }
const label = words[ctx.i18n.locale.split('-')[0]] ?? 'Total'
const count = ctx.i18n.formatNumber(ctx.graph.getNodes().length)
// …draw `${label}: ${count}`
},
}What stays as it is
- Your data. Node and edge labels, property names and values are drawn as they are. A property name without a declared label is title-cased from its key (
first_seenreads "First Seen"); declarenodePropertiesMaporedgePropertiesMapto name it in your language. Theidrow the property lists add above your own keys is written as the key it is. - Developer text. Console warnings and thrown errors stay in English.
- Key glyphs.
⌃ ⇧ ⌥ ⌘are shown as they are. Key names are messages (keys.ctrlCombo,keys.escape), so a German interface can sayStrg+Z. - Badge counts on a node's rim (
2k,99+), which must fit in three characters.