Never Concatenate Labels

This really doesn't end well with languages like Japanese, where sentence structure can be radically different. Instead, use replacement tokens and String.format (or equivalent).

In other words, labels like: Hi, I'm a label for {0} where you replace {0} with a different label as needed are good. While doing Hi, I'm a label for '+'something' is terrible and will cause horrible pain.

If you apply formatting or hyperlinks to a subsection of your label this task becomes a lot harder than it sounds.

  • In lightning: make use of ui:outputRichText. This will allow formatting HTML, to the same standards as rich text fields in the native UI, but strip all sorts of permutations of exploits or scripting out. Links work too. Do not use aura:unescapedHTML for this!

    In my case where I did need formatting and token replacement I ended up having to define a helper for my component that builds the ready to use strings, and adding <aura:attribute type="Object" name="complexLabels" />. Without this, you'll get warnings (and later on errors) about access checks, since you can't dynamically create new view properties in lightning, but you can add new keys to an Object. In the helper, process and build the labels, then do something like cmp.set('v.complexLabels', {}); and cmp.get('v.complexLabels').exampleLabel = 'blah';

    While outputRichText takes care of filtering out things like XSS attempts, you still make way to do HTML escaping on the $Label values to prevent people from doing silly things like adding image tags in label translations.

  • In visualforce: you can prepare labels in the view pretty simply: make use of apex:outputText's ability to accept apex:param values in a String.format-like way, as well as HTMLENCODE in merge expressions.

<apex:outputText escape="false" value="{!HTMLENCODE($Label.labelWithReplacementTokens)}">  
    <apex:param value="<b>{!HTMLENCODE($Label.toReplaceWith)}</b>" />

or, in terms more practical for a demo:

<apex:page showHeader="false">  
    <apex:outputText escape="false" value="{!HTMLENCODE('<i>this text will not be italicized. but this will be bolded: {0}</i>')}">
        <apex:param value="<b>{!HTMLENCODE('see?')}</b>" />

which nets you:

<i>this text will not be italicized. but this will be bolded: see?</i>

Don't Forget Custom Objects

The translation workbench doesn't include custom objects labels, or their Name field. To get these you can

  • Create a tab for your custom object, then use the setup UI: Setup > Customize > Tab Names And Labels > Rename Tabs and Labels
  • Use the metadata API (no tabs required).

Considering you're likely putting this all into source control anyways it seems like the metadata API is the way to go. These translations are stored alongside custom field translations (the kind you use the translation workbench for) in the CustomObjectTranslation metadata type. The various permutations (masculine/feminine, singular/plural, and all other sorts of language specific variants) go into the caseValues element.

Read the docs for the full details, but as something to get you started, you're likely going to need to add/edit something like this in your object translation metadata files:

<nameFieldLabel>Name Field Label</nameFieldLabel>  
     <value>My Object Label</value>
     <value>My Object Labels</value>
Lightning Doesn't Include Labels in Packages

This is a known, and documented, limitation today:

Custom labels referenced in a Lightning component are not automatically added when you create a package containing the Lightning component. To include a custom label in a package, another member of the package, such as a Visualforce page, must reference the custom label.

Still, it blindsides anybody who doesn't RTFM sufficiently (like me a couple months ago). To get these labels into your package you need to reference them in visualforce or apex. Creating an @isTest apex class for this with no test methods is great, since you don't have to get test coverage for your labels then. Just make sure you add it to your package, since as a test class nothing will depend on it.

Think About De-Protecting Your Labels

If you're delivering your app as a managed package that you expect your end users to customize, think about removing the protected checkbox on your labels. The cons of this are that you can't delete labels later, but the upside is that customers can reference your packaged labels in their customization. This will save them the pain (and expense) of having to have two sets of labels for the same text.

In a non-translation related note, this also makes declarative customization much easier. Protected labels have a cascading nature, so if you use one in a formula field your customers won't be able to make their own formula fields referencing yours!

If you're worried about running out of label space, be aware it is a soft limit, although you shouldn't go crazy just because of that!