Resources and Themes
Goal: At the end of this chapter your app speaks two languages, takes its colours from one central file, and switches between a dark and a light look at the press of a button.
Say it once
Look at the business card from two chapters ago. The yellow "#F2C14E" is written into the name label. If a second label should have the same yellow, you write it again. If you then want a slightly different yellow, you have to find every copy.
The same is true for texts. text: "Say hello" is fine – until someone wants the app in German.
Resources solve both problems. You give a value a name, write it down once in a separate file, and use the name everywhere else. SML has four kinds of resources:
| Reference | Defined in | Used for |
|---|---|---|
@Strings.name |
strings.sml, strings-de.sml, … |
every text the user reads |
@Colors.name |
theme.sml |
colours |
@Layouts.name |
theme.sml |
numbers like font sizes |
elevation: name |
theme.sml, block Elevations |
a whole style: corners, shadow |
All these files sit next to app.sml. The Runner finds them by their names; there is nothing to register.
The example
A welcome card in English and German, with a Light and a Dark button. This project has more files than the ones before:
welcome/
├── app.sml
├── app.sms
├── strings.sml
├── strings-de.sml
├── theme.sml
├── theme.light.sml
└── theme.dark.sml
strings.sml – the texts, in English:
Strings {
windowTitle: "Welcome"
headline: "Welcome to Forge"
intro: "This text comes from strings.sml."
btnLight: "Light"
btnDark: "Dark"
themeChanged: "Theme changed"
}
strings-de.sml – the same names, in German:
Strings {
windowTitle: "Willkommen"
headline: "Willkommen bei Forge"
intro: "Dieser Text kommt aus strings-de.sml."
btnLight: "Hell"
btnDark: "Dunkel"
themeChanged: "Farben gewechselt"
}
theme.sml – the design of the app:
// Design tokens of this app. Used in app.sml as @Colors.name and @Layouts.name.
Colors {
brand: "#F2C14E"
surface: "#1B2433"
text: "#E8ECF2"
}
Layouts {
titleSize: 30
}
Elevations {
card {
borderRadius: 12
shadowColor: "#00000080"
shadowSize: 8
}
}
theme.light.sml and theme.dark.sml – two sets of colours to switch between:
Colors {
brand: "#B5541B"
surface: "#F4F1EA"
text: "#1B2433"
}
Colors {
brand: "#F2C14E"
surface: "#1B2433"
text: "#E8ECF2"
}
app.sml – no text and no colour is written here directly:
Window {
id: mainWindow
title: @Strings.windowTitle
size: 520, 320
VBoxContainer {
anchors: left | top | right | bottom
padding: 24, 24, 24, 24
PanelContainer {
id: card
elevation: card
bgColor: @Colors.surface
sizeFlagsVertical: expandFill
VBoxContainer {
padding: 24, 20, 24, 20
spacing: 12
Label { id: headline text: @Strings.headline fontSize: @Layouts.titleSize color: @Colors.brand }
Label { id: intro text: @Strings.intro color: @Colors.text }
Label { id: missing text: @Strings.notThere, "Fallback text" color: @Colors.text }
HBoxContainer {
spacing: 8
Button { id: btnLight text: @Strings.btnLight }
Button { id: btnDark text: @Strings.btnDark }
}
}
}
}
}
app.sms:
on btnLight.clicked() {
ui.setTheme("light")
log.info(i18n.tr("themeChanged"))
}
on btnDark.clicked() {
ui.setTheme("dark")
log.info(i18n.tr("themeChanged"))
}
Start it as usual. Then start it once more, this time pretending to be on a German system:
LANG=de_DE.UTF-8 ./run.sh url file://$HOME/forge/welcome/app.sml

Strings and languages
title: @Strings.windowTitle
A value that starts with @ is a reference. @Strings.windowTitle means: look up windowTitle in the strings and use what you find there.
At start, the Runner reads strings.sml. Then it looks at the language of the system – the first two letters of the LANG setting, for example de from de_DE.UTF-8 – and if a file strings-de.sml exists, it reads that one too. Every name in it replaces the English one. Names that are missing in the German file simply stay English. You can translate an app step by step.
A new language is a new file: strings-es.sml for Spanish, strings-fr.sml for French. Nothing else changes.
Two rules keep this working:
- The names (
headline,btnLight) are the same in every language file. Only the texts differ. strings.smlis complete. It is the fallback for every language that has no file, and for every name a translation forgot.
Fallbacks
Label { id: missing text: @Strings.notThere, "Fallback text" color: @Colors.text }
There is no notThere in any strings file. Without a fallback, the label would be empty. With , "Fallback text" after the reference, the Runner uses that text instead. This is handy while you build an app: you can write the English text right into the SML file and move it into strings.sml later.
Texts in the script
The script uses the same strings:
log.info(i18n.tr("themeChanged"))
i18n.tr("themeChanged") returns the text for themeChanged in the current language – Theme changed or Farben gewechselt. ("i18n" is the usual abbreviation for internationalisation: an i, eighteen letters, an n.) If the name is not found, you get the name itself back, so a missing translation is visible but never crashes anything. A second argument is a default text: i18n.tr("greeting", "Hello").
Colours and numbers
Label { id: headline text: @Strings.headline fontSize: @Layouts.titleSize color: @Colors.brand }
@Colors.brand and @Layouts.titleSize work exactly like strings, but they come from theme.sml. Colors holds colours, Layouts holds numbers.
Names like brand, surface and text describe a role, not a colour. Do not call a token yellow: the day the brand colour becomes orange, the name lies. In a light theme, text is dark and surface is light; in a dark theme, the other way round. The roles stay the same.
Elevations: a whole style with one word
Elevations {
card {
borderRadius: 12
shadowColor: "#00000080"
shadowSize: 8
}
}
PanelContainer {
id: card
elevation: card
bgColor: @Colors.surface
An elevation is a named group of style properties. elevation: card copies all of them onto the panel: round corners, a soft shadow. Every card in your app gets the same look from one line, and when you change borderRadius in the theme, all cards change.
The node can still add or override properties of its own – here bgColor. Node properties always win over the elevation.
The style properties you can put into an elevation are the ones that draw a box: bgColor, borderColor, borderWidth, borderRadius, shadowColor, shadowSize, shadowOffsetX, shadowOffsetY.
(#00000080 is a colour with four pairs of digits: the last pair is the opacity. 80 is about half – a black that lets half of the background through.)
Switching themes
on btnLight.clicked() {
ui.setTheme("light")
}
ui.setTheme("light") reads theme.light.sml and gives every @Colors reference the new value – while the app is running.

The theme name is part of the file name: ui.setTheme("dark") reads theme.dark.sml, ui.setTheme("sepia") would read theme.sepia.sml. If that file does not exist, the Runner goes back to theme.sml.
What changes when you switch: the color, bgColor and borderColor of every node that has an id and uses a @Colors reference. That is why the labels and the card have ids, even though the script never talks to them by name. The buttons and the window background belong to the Runner's own theme and keep their look.
Pitfalls
A text stays English on a German system. The name in strings-de.sml is spelled differently from the one in strings.sml. Names must match exactly, including upper and lower case.
A colour does not change when you switch the theme. The node has no id, or its colour is written directly ("#F2C14E") instead of as @Colors.…. Or the colour comes from an elevation: elevations are applied once, at start. Put the colour that should switch onto the node itself, as bgColor: @Colors.surface in the example.
@Colors.brand and the label is white. The name is not in theme.sml. The Runner writes a warning (@Colors.brand not found) to the terminal – read it.
The resource files are not found. They must be in the same folder as app.sml, and they must be called exactly strings.sml, strings-de.sml, theme.sml, theme.light.sml.
LANG=de does not switch. The Runner reads the language from the environment variables LANGUAGE, LC_ALL, LC_MESSAGES and LANG, in this order. If LANGUAGE is set to English on your system, it wins. In the current Runner this is the only source: an app started from the macOS Finder, which gets none of these variables, shows the English texts.
Exercise
- Add a Spanish translation:
strings-es.sml. Start the app withLANG=es_ES.UTF-8and check every text. - Add a third theme,
theme.sepia.sml, with warm brown colours, and a third button Sepia that switches to it. Its text comes from the strings files, in all three languages. - Move the
"Fallback text"into the strings files under the namenotThere, and remove the fallback fromapp.sml.
The solution is in the appendix.
What you learned
- A value that starts with
@is a reference to a named resource. strings.smlholds the texts;strings-de.sml,strings-es.sml… translate them. The system language picks the file.@Strings.name, "text"has a fallback;i18n.tr("name")gets a text in the script.theme.smlholdsColors,LayoutsandElevations. Name tokens by role (brand,surface,text), not by colour.elevation: cardapplies a whole named style; node properties win over it.ui.setTheme("light")loadstheme.light.smland recolours every node with an id and a@Colorsreference.
Next: Markdown inside SML
