Skriva appar (plugins)

Appar lägger till funktioner i Antimatter inifrån: en serverdel i Go som kopplar in sig i servern, och en webappdel i React som lägger till menyer, paneler, meddelandetyper och sidor. Denna sida visar hur en app är uppbyggd, med den medföljande appen Omröstningar som exempel, och vad som ändras i Antimatters Fusion-gränssnitt.

Så här fungerar en app

  • Serverdelen är ett Go-program. Servern startar det som en separat process och kommunicerar med det via RPC: den anropar appens hooks (ett meddelande ska publiceras, ett kommando körs …) och appen anropar serverns plugin API (skapa ett inlägg, läsa en användare, lagra data …). Den kan också servera HTTP-rutter under /plugins/<app id>/.
  • Webappdelen är en JavaScript-bundle som laddas in i webbläsaren tillsammans med webappen. Den registrerar React-komponenter och callback-funktioner i registry: en knapp i en meny, en panel, ett sätt att rita ut en meddelandetype.
  • Varje del är valfri. En app installeras som en .tar.gz-bundle, och systemadministratörer aktiverar, inaktiverar och konfigurerar den.
Serverdelen körs med full tillgång till serverns data. Behandla en app som kod som du deployar till servern.

Antimatter kör appar som är skrivna för Mattermost, och tvärtom, så länge en app endast använder hooks som båda har. Se Hooks som endast Antimatter har.

Bundle-layout

Det finns inget separat startmall: börja med en liten medföljande app som antimatter-plugin-polls eller antimatter-plugin-todo, och ändra dess ID och kod. Omröstningar är uppbyggd så här:

antimatter-plugin-polls/
├── plugin.json            the manifest
├── Makefile               build, test, bundle, deploy
├── build/                 the manifest and pluginctl helper tools (Go), setup.mk
├── assets/icon.svg        files shipped with the bundle
├── go.mod
├── server/
│   ├── main.go            plugin.ClientMain(&Plugin{})
│   ├── plugin.go          the Plugin type, OnActivate, ServeHTTP
│   ├── manifest.go        generated from plugin.json
│   ├── api.go, command.go, hooks.go, store.go …
│   └── dist/              compiled binaries, one per platform
└── webapp/
    ├── package.json, webpack.config.js, tsconfig.json
    ├── i18n/              en.json and one file per language
    ├── src/index.tsx      registers the app with the web app
    ├── src/manifest.ts    generated from plugin.json
    ├── src/translations.ts
    └── dist/main.js       the bundle

Valfritt: en public/-mapp, som serveras som den är på /plugins/<app id>/public/.

plugin.json

{
  "id": "com.example.weather",
  "name": "Weather",
  "description": "Forecasts in the chat.",
  "homepage_url": "https://github.com/example/weather",
  "support_url": "https://github.com/example/weather/issues",
  "icon_path": "assets/icon.svg",
  "version": "1.0.0",
  "min_server_version": "12.0.0",
  "server": {
    "executables": {
      "linux-amd64": "server/dist/plugin-linux-amd64",
      "linux-arm64": "server/dist/plugin-linux-arm64",
      "darwin-amd64": "server/dist/plugin-darwin-amd64",
      "darwin-arm64": "server/dist/plugin-darwin-arm64",
      "windows-amd64": "server/dist/plugin-windows-amd64.exe"
    }
  },
  "webapp": {"bundle_path": "webapp/dist/main.js"},
  "settings_schema": {"settings": []}
}
  • id: unik och permanent; ett omvänt domännamn är vanligt. Det namnger rutter, de lagrade inställningarna och WebSocket-händelserna.
  • version: semantisk version. De medföljande apparna utesluter den från plugin.json; deras byggverktyg hämtar den från den senaste v*-git-taggen.
  • min_server_version: Antimatter rapporterar 12.0.0.
  • settings_schema: appens inställningssida i Systemkonsolen (se Inställningar).

make apply kopierar manifestet till server/manifest.go och webapp/src/manifest.ts; make manifest-check validerar det.

Serverdelen

Serverdelen importerar samma Go-modul som Mattermost-appar, github.com/mattermost/mattermost/server/public, från Go-modulproxyn. Dess main startar bara appen:

package main

import "github.com/mattermost/mattermost/server/public/plugin"

func main() {
	plugin.ClientMain(&Plugin{})
}

Typen Plugin bäddar in plugin.MattermostPlugin, vilket ger den p.API, och implementerar de hooks den behöver:

type Plugin struct {
	plugin.MattermostPlugin
	router *mux.Router
}

func (p *Plugin) OnActivate() error {
	p.router = p.newRouter()
	return p.API.RegisterCommand(&model.Command{
		Trigger:          "weather",
		AutoComplete:     true,
		AutoCompleteDesc: "Show the forecast",
		AutoCompleteHint: "[city]",
	})
}

func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	city := strings.TrimSpace(strings.TrimPrefix(args.Command, "/weather"))
	return &model.CommandResponse{
		ResponseType: model.CommandResponseTypeEphemeral,
		Text:         "Forecast for " + city + ": sunny",
	}, nil
}

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
	p.router.ServeHTTP(w, r)
}

Vanliga hooks

HookNär den körs
OnActivate, OnDeactivateNär appen aktiveras eller inaktiveras, samt när servern startar och stängs av. Registrera kommandon och botar i OnActivate.
OnConfigurationChangeNär dess inställningar ändras; ladda dem med p.API.LoadPluginConfiguration(&cfg).
ServeHTTPBegäranden till /plugins/<app id>/…. Servern sätter Mattermost-User-Id-rubriken när begärandet kommer från en inloggad användare; neka begäranden utan den om de inte är avsedda att vara publika.
ExecuteCommandNär ett kommando som appen registrerat körs.
MessageWillBePosted, MessageWillBeUpdatedInnan ett meddelande sparas: returnera ett ändrat inlägg eller en anledning att neka det.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedI efterhand, för biverkningar.
ReactionHasBeenAdded, ReactionHasBeenRemovedReaktioner.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedÄndringar av medlemskap och kanaler. Ett team är en gemenskap.
FileWillBeUploadedInnan en fil lagras: ändra den eller neka den.
OnPluginClusterEventEtt meddelande från samma app på en annan server i en kluster.

Den fullständiga listan, med exakta signaturer, finns i server/public/plugin/hooks.go; API:t finns i server/public/plugin/api.go. Paketet pluginapi (server/public/pluginapi) kapslar in API:t i vänligare hjälpfunktioner.

Användbara API-anrop

  • CreatePost, UpdatePost, SendEphemeralPost: inlägga som appens bot eller som en användare.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): skapa appens bot en gång och få dess ID.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): skicka ett händelse till webbappen. Den kommer in som custom_<app id>_poll_updated.
  • OpenInteractiveDialog: öppna en dialogruta.
  • LogInfo, LogError och vänner: skriva till serverlogg, taggat med appens ID.
  • GetUser, GetChannel, HasPermissionToChannel: kontrollera vem som får göra vad innan du agerar för en användare.

En app körs på varje server i en kluster. Spara tillstånd i nyckel-värde-lagret eller i databasen, inte bara i minnet.

AI-tjänsten

Antimatters server anropar AI-modeller för appar via de modellanslutningar som en administratör har konfigurerat (se AI i administratörguiden). En app förvarar aldrig en leverantörs nyckel: den anger vilken person den agerar för och var, och servern kontrollerar att personen får använda anslutningen där, tillämpar dess begränsningar och budgetar, räknar token mot personen och appen, samt granskar anropet utan dess innehåll.

MetodVad den gör
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)En chattfullbordan: meddelanden, verktyg, strukturerad output (JSONSchema).
AIChatStream(…)Samma, strömmat: läs strömmens Events tills kanalen stängs, eller Close den för att stoppa anropet.
AIEmbed(…, *ai.EmbedRequest)Embeddings av texter.
AITranscribe(…, *ai.TranscribeRequest)Tal till text.
AIDecide(…, *ai.DecideRequest)Typade frågor till ett beslutsmodell (ja eller nej, ett val, ett betyg) om en text.
AIConnectionsForUser(userID, channelID, teamID)De anslutningar en person får använda där, med deras modeller och funktioner, aldrig deras nycklar.

Varje anropets ConnectionID väljer anslutningen, och Model en av dess modeller (tomt för anslutningens standard). Operation är en kort etikett för användnings- och granskningsregister, aldrig innehåll. Deras minsta serverversion är 11.11.

Använd actingUserID endast från en källa som servern verifierat: Mattermost-User-Id-rubriken i ett anrop till din ServeHTTP, eller användaren i en hooks kontext (ett inläggs författare, användaren som körde ett kommando). Ta aldrig ifrån ett meddelande, en webhook-payload, en URL eller en annan apps anrop: din app skulle annars använda någon annans åtkomst.

Dessa metoder och ai-paketet (server/public/ai) finns endast i Antimatters kopia av modulen, inte i versionen publicerad av Mattermost. För att använda dem, bygg mot Antimatters server/public med en replace-rad i din go.mod, till exempel replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. En sådan app körs endast på Antimatter.

Lagring av data

Varje app har sitt eget nyckel-värde-lager i serverns databas:

  • KVSet(key, value), KVGet(key), KVDelete(key): värden är byte, vanligtvis JSON. Nycklar är högst 150 tecken.
  • KVSetWithExpiry(key, value, seconds): värdet försvinner efter den tiden.
  • KVCompareAndSet(key, old, new) och KVSetWithOptions med Atomic: säkra uppdateringar när flera begäranden eller servrar skriver samma nyckel. Polls sparar varje röst på detta sätt och gör om vid konflikt.
  • KVList(page, perPage): nycklarna, sida för sida.

Webbapp-delen

Bundlen registrerar en klass med window.registerPlugin. Dess initialize tar emot registret och Redux-lagret:

import manifest from './manifest';

class Plugin {
    initialize(registry, store) {
        registry.registerTranslations((locale) => translationsFor(locale));
        registry.registerPostTypeComponent('custom_poll', PollPost);
        registry.registerWebSocketEventHandler(`custom_${manifest.id}_poll_updated`, (msg) => {
            store.dispatch(pollUpdated(JSON.parse(msg.data.poll)));
        });
        registry.registerReconnectHandler(() => store.dispatch(reloadPolls()));
    }
}

window.registerPlugin(manifest.id, new Plugin());
  • Delade bibliotek. Webbappen tillhandahåller React, ReactDOM, Redux, React Redux och React Intl: ange dem som webpack externals (React, ReactDOM, Redux, ReactRedux, ReactIntl) istället för att bundla egna kopior. Se Polls' webpack.config.js.
  • Anrop av din serverdel. Hämta ${window.basename}/plugins/<app id>/… med alternativen från Client4.getOptions() (från mattermost-redux/client): de bär med sig de huvuden som servern behöver för att acceptera en begäran från webbläsaren.
  • Ditt eget tillstånd. registerReducer lägger till en reducer, som sparas under state['plugins-<app id>'].
  • Typen. Antimatters extra registreringsmetoder finns inte i Mattermosts publicerade typer: Polls deklarerar det register det använder i webapp/src/types/host.ts.

Registrets metoder och deras parametrar är dokumenterade i koden, i webapp/fusion/src/plugins/registry.ts (Fusion) och webapp/channels/src/plugins/registry.ts (klassisk).

Stöd för Fusion-gränssnittet

Antimatter har två webbgränssnitt: Fusion, som är standard, och det klassiska. Båda laddar samma appbundlar och har samma register, men Fusion ritar många saker själv och placerar komponenter från appar på sitt eget sätt. Testa din app i båda.

Identifiera gränssnittet

Innan någon app laddas, sätter båda gränssnitten window.antimatterWebUI till 'fusion' eller 'classic', och samma värde på sidans rot-element som data-am-web-ui. I Mattermost är varken satt.

export function isFusionUI(): boolean {
    return window.antimatterWebUI === 'fusion' || document.documentElement.dataset.amWebUi === 'fusion';
}

De medföljande apparna använder detta för att renderas annorlunda. Omröstningar ritar sina kort och sitt skapelsedialog i Fusion-stil och öppnar ett eget dialog för /poll i Fusion. Samtal och röstkanaler utesluter sidofältet och toast-komponenterna som Fusion byter ut mot sitt eget samtalsgränssnitt. Agent-appen placerar sina AI-åtgärder i meddelandemenyn, som en grupp under Fler åtgärder, istället för förhållningsverktygsfältet. Fusions egna CSS-klasser (med prefixet am-) och dess dialoglager (elementet am-layer) är det som dessa appar använder för att matcha gränssnittet; de är inte en dokumenterad, stabil API.

Var Fusion visar plugin-komponenter

RegistreringsmetodVar det visas i Fusion
registerAppBarComponent, registerChannelHeaderButtonActionAppspåret, kolumnen med ikoner på höger kant, mellan din avatar och knappen Om Antimatter. Kanalrubrikknappar hamnar också där, inte i kanalrubriken. Dold när en systemadministratör aktiverar Inaktivera Apps Bar (ExperimentalSettings.DisableAppBar, inaktiverad som standard).
registerRightHandSidebarComponentPanelen till höger. Användare kan ändra dess storlek genom att dra i kanten och öppna den i ett nytt fönster. Om appen också är en produkt erbjuder panelen Öppna som helsida. Se Panelen i ett nytt fönster.
registerChannelHeaderIconIkoner i kanalens huvud, efter samtalssknapparna; på telefoner, i huvudets Mer-meny.
registerCallButtonActionKanalens huvud samtalssknapp; flera leverantörer delar en Andra sätt att ringa-meny.
registerPostDropdownMenuActionMeddelandets Åtgärder för meddelanden (⋯) -meny, i dess undermeny Fler åtgärder; även i trädpanelens Trådåtgärder -meny, under Fler åtgärder. Fusion ritar appens ikon före varje åtgärd, i grått: dess egen ikon för de medföljande apparna, annars den ikon appen visar i appspåret, annars en neutral. Ett element utan text i början av åtgärdens text (en ikon appen ritar själv) göms.
registerPostDropdownSubMenuActionEn grupp med en rubrik i samma undermeny Fler åtgärder, efter de enskilda åtgärderna. Ingen app får ett eget objekt i meddelandemenyn.
registerPostActionComponentMeddelandets förhandsvisningsverktygsfält, före ⋯-knappen.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentSjälvständigt meddelande: dess innehåll, under dess text, efter dess tid, och i stället för länkförhandsvisningar.
registerChannelHeaderMenuActionKanalens Kanalalternativ -meny (⋯ på en kanal i sidofältet, eller högerklick), som objekt med en stickkontakt-ikon.
registerMainMenuActionGemenskapens meny (gemenskapens namn i toppen av kanallistan), under Fler åtgärder, grupperad per app.
registerSidebarBrowseOrAddChannelMenuActionGemenskapens meny, efter Skapa kanal och Skapa kategori.
registerFileUploadMethodKomponerarens + -meny (Lägg till i meddelandet), efter Ladda upp en fil.
registerPostEditorActionComponentSkrivfältets formateringsfält, när formatering visas.
registerAIActionMenuItemComponentSkrivfältets AI-meny.
registerFileDropdownMenuAction, registerFilePreviewComponentEn filens inställningsmeny, och filens förhandsvisning.
registerUserSettingsProfil & inställningar, en flik per app under rubriken Plugins.
registerChannelSettingsTabKanalinställningssammanhanget, under rubriken Appar.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentProfilkortet som visas när du klickar på ett namn eller en avatar.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentÖverst i kanallistan för en gemenskap, bredvid kanalens namn, kanalens ikon och längst ner i gemenskapslistan till vänster.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderBörjan av kanalens historik, överst i meddelandelistan, olästlinjen, ovanför meddelandefältet och meddelandefältets platshållare.
registerChannelTypeOptionKanaltyperna i dialogrutan Skapa kanal.
registerSearchComponentsSökfältet i kanalens sidhuvud: appomfång visas när det är tomt.
registerUserGuideDropdownMenuActionOm Antimatter (längst ner i appmenyn), under Hjälp.
registerProductHelsidor på produktens URL, listade under Produkter i Om Antimatter. Produktsidor behåller den klassiska sidramen, vars produktmeny endast visas när en app registrerar en produkt eller lägger till ett objekt i den. De medföljande apparna Kanban och Playbooks registrerar inte längre produkter: deras sidor öppnas i deras sidopaneler.
registerRootComponent, registerGlobalComponent, hooks såsom registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslationsFungerar som i det klassiska gränssnittet.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingSystemkonsolen, som är den klassiska i båda gränssnitten.

Det Fusion inte visar eller kör

  • registerPostDropdownMenuComponent (en komponent i meddelandemenyn), registerPostCardTypeComponent (ett meddelandekort i panelen till höger) och registerProductSwitcherMenuItem visas inte i Fusions huvudvyer. Använd registerPostDropdownMenuAction istället för den första.
  • registerMessageWillBeUpdatedHook körs inte när ett meddelande redigeras i Fusion. Kontrollera redigeringar på servern med hooken MessageWillBeUpdated istället.
  • registerActionAfterChannelCreation körs endast från den klassiska dialogrutan för kanalskapning, som Fusion öppnar från Fler alternativ… i dialogrutan Skapa kanal.
  • Komponenter i appmenyn visas oavsett deras supportedProductIds.
  • Sökvägar från registerNeedsTeamRoute och registerCustomRoute behåller den klassiska sidramen, liksom produkter.

Hooks som endast Antimatter har

Dessa registreringsmetoder finns inte i Mattermost. Kontrollera att en metod finns innan du anropar den, så att appen också fungerar där:

if (typeof registry.registerComposerMenuItem === 'function') {
    registry.registerComposerMenuItem({
        text: 'Create a poll',
        icon: <i className='icon icon-chart-bar'/>,
        fusionIcon: 'poll',
        action: ({channelId, rootId}) => openPollCreation(channelId, rootId),
    });
} else {
    registry.registerChannelHeaderButtonAction(icon, (channel) => openPollCreation(channel.id), 'Create a poll', 'Create a poll');
}
MetodVad den lägger till
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})En post i meddelandefältets +-meny, i båda gränssnitten. action tar emot {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})En flik i meddelandefältets emojivaljare, till exempel GIF:er eller klistermärken. I Fusion kommer flikar med ett order-värde under 10 före fliken Emojier. Komponenten tar emot channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion och, i Fusion, pickerKit (se nedan).
registerChannelViewPanel({matcher, component})En panel mellan kanalens rubrik och meddelandena, för de kanaler som matcher(state, channel) väljer. Den kan dölja meddelandena och meddelandefältet (setMessagesVisible), precis som röstkanaler gör.
registerSidebarChannelFooterComponent(component)En rad under en kanal i sidofältet, till exempel personerna i dess samtal. Den tar emot channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})Endast Fusion: en post på sidorna Integrationer, intill anpassade emojier. GIF-appen lägger till sina anpassade GIF:er och klistermärken där.

Lägg till och hantera vyer i emojivaljaren

I Fusion får en flik i emojivaljaren egenskapen pickerKit: de komponenter som fliken Emojier använder för sina egna vyer för att lägga till och hantera, så att en fliks vyer ser likadana ut och förblir inuti valjaren. Apparna GIF:er och Memes använder den för att lägga till och hantera GIF:er, klistermärken och memmallar. Den innehåller version (1), hookarna usePickerView och useOnEscape, samt komponenterna PickerViewButtons (knapparna + och Hantera bredvid sökfältet), PickerViewFrame (en vy med dess bak-pekare), PickerNotice, PickerField, PickerSearch, PickerDropZone och PickerManageList. Escape går tillbaka från en vy till flikens rutnät. Kontrollera att egenskapen finns: andra värdar utesluter den, och då behåller fliken sina egna dialogrutor. Typerna finns i webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) och komponenterna i webapp/fusion/src/fusion/popovers/picker_views.tsx.

Panelen i ett nytt fönster

Fusion ger komponenten för registerRightHandSidebarComponent två ytterligare egenskaper, så att Öppna i nytt fönster öppnar panelen där den var istället för på appens startsida:

  • setPanelState(state): anropa den när panelen byter plats, med en kort sträng som din app definierar, till exempel 'note/<id>' eller 'board/<id>/card/<id>', eller null på dess startsida. Den hamnar i fönstrets adress, så lägg aldrig in en hemlighet i den. En tillstånd som är längre än 512 tecken, eller som innehåller styrtecken, kasseras.
  • initialPanelState: i det nya fönstret, det tillstånd som panelen hade, eller undefined. Behandla det som opålitlig indata: hämta objektet igen som den inloggade användaren, och visa appens startsida eller en vy för att objektet inte hittades om de inte kan öppna det.

Tillståndet glöms bort när panelen stängs eller när en annan apps panel tar dess plats. För att själv öppna ett panels fönstert, popoutRhsPlugin (även i window.WebappUtils.popouts) tar tillståndet som ett valfritt femte argument. Anteckningar, Whiteboard, Att göra, Kanban och Playbooks använder detta.

fusionIcon anger en ikon ur Fusions ikonuppsättning (till exempel poll, sticker, calendar eller image; se webapp/fusion/src/fusion/components/icon). Fusion visar den istället för icon, så att objektet matchar de andra.

Inställningar

settings_schema i plugin.json beskriver appens sida i Systemkonsol › Plugins:

"settings_schema": {
  "header": "Forecasts come from the weather service below.",
  "settings": [
    {"key": "ServiceURL", "display_name": "Service URL", "type": "text", "default": "https://api.example.com"},
    {"key": "Units", "display_name": "Units", "type": "dropdown", "default": "metric",
     "options": [{"display_name": "Celsius", "value": "metric"}, {"display_name": "Fahrenheit", "value": "imperial"}]},
    {"key": "EnableAlerts", "display_name": "Severe weather alerts", "type": "bool", "default": false,
     "help_text": "Post alerts in the channels that follow a city."}
  ]
}

Inställningstyper: bool, text, longtext, number, dropdown, radio, generated (en slumpmässig hemlighet), username och custom (ritad av en komponent som webbapplikationsdelen registrerar med registerAdminConsoleCustomSetting). Inställningar kan grupperas i sections.

Serverdelen läser in dem i en struktur vars fält matchar nycklarna:

type configuration struct {
	ServiceURL   string
	Units        string
	EnableAlerts bool
}

func (p *Plugin) OnConfigurationChange() error {
	var cfg configuration
	if err := p.API.LoadPluginConfiguration(&cfg); err != nil {
		return err
	}
	p.setConfiguration(&cfg)
	return nil
}

Administratörer kan också ställa in dem i konfigurationen, under PluginSettings.Plugins.<app id>, med nycklar i gemina bokstäver (se administratörguiden).

Översättningar

Antimatter är översatt till 37 språk, och medföljande appar är det också. Webbapplikationsdelen gör det så här:

  1. Skriv varje sträng med React Intl, med en ID och dess engelska text: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> eller defineMessages. Lägg appens namn som prefix på ID:erna.
  2. Extrahera de engelska strängarna till webapp/i18n/en.json (medföljande appar använder formatjs extract).
  3. Lägg till en fil per språk intill den, med samma ID:er: fr.json, de.json, pt-BR.json, zh-CN.json…
  4. Ge dem till webbappen, som frågar efter användarens språk:
    import fr from '../i18n/fr.json';
    import ptBR from '../i18n/pt-BR.json';
    
    const catalogs = {fr, 'pt-BR': ptBR};
    
    export function translationsFor(locale) {
        return catalogs[locale] || catalogs[locale.split('-')[0]] || {};
    }
    
    registry.registerTranslations(translationsFor);

En saknad sträng faller tillbaka på sin engelska defaultMessage. För strängar som serverdelen skickar (kommandosvar, botmeddelanden) läser paketet pluginapi/i18n in översättningsfiler som medföljer i bunlen (till exempel assets/i18n) och ger en lokaliserare på varje användares språk.

Bygg, installera och felsök

Du behöver Go och Node.js (versionen i appens .nvmrc). I appens mapp:

KommandoVad den gör
make distBygger serverdelen för alla plattformar och webbapplikationsdelen, och skriver bunlen till dist/<app id>-<version>.tar.gz.
make testKör Go- och JavaScript-testerna.
make check-styleLinterar koden (golangci-lint, ESLint) och kontrollerar TypeScript-typerna.
make deployBygger, installerar och aktiverar sedan bunlen på en server.
make watch, make deploy-from-watchDen första bygger om webbapplikationsdelen varje gång du sparar; den andra bunlar det den byggt och installerar det på servern.
make enable, make disable, make resetAktivera, inaktivera eller starta om appen på servern.
make logs, make logs-watchVisa appens rader i serverloggen.
make attachFäster Delve-felsökaren vid den körande serverdelen (bygg först med AM_DEBUG=1).

make deploy och de andra serverkommandona använder en lokal modus-socket när de hittar en (/var/tmp/antimatter_local.socket, eller AM_LOCALSOCKETPATH): kör dem på serverns maskin med lokal modus på. Annars loggar de in på servern som anges av MM_SERVICESETTINGS_SITEURL med AM_ADMIN_TOKEN, eller AM_ADMIN_USERNAME och AM_ADMIN_PASSWORD. Byggnadsverktygen läser inte ännu ett AM_-namn för server-URL:en.

Installera en bunl manuellt

  • En systemadministratör tillåter uppladdningar med AM_PLUGINSETTINGS_ENABLEUPLOADS=true (av som standard; det finns ingen växlar i Systemkonsolen).
  • Därefter Systemkonsol › Plugin › Pluginadministration › Ladda upp plugin, eller amctl plugin add weather.tar.gz följt av amctl plugin enable com.example.weather.
  • Om servern har Kräv Plugin-signatur aktiverat, accepterar den endast paket som är signerade med en nyckel den litar på; se Installation av andra appar.
För snabba iterationer på en utvecklingsserver, sätt AM_SERVICESETTINGS_ENABLEDEVELOPER=true på servern: felmeddelanden bär då med sig sina detaljer. De medföljande apparnas Makefiles bygger serverdelen endast för din plattform när MM_SERVICESETTINGS_ENABLEDEVELOPER är satt i din skal.