Language packs enable you to provide additional languages with your app without increasing the initial download size.
These files are uploaded to the Meta Horizon Store when you upload your app package.
For Link PC-VR devices
On Link PC-VR devices, providing language packs as downloadable assets will decrease initial download size. The Meta Horizon Link app for Link PC-VR will allow users to select which language pack they want to use from the Details page.
The following image shows an example of how this looks.
For Meta VR devices
On Meta VR devices, providing language packs as downloadable assets will decrease initial download size.
Developers will have to implement their own language picker in an app. Then, users will be able to select a language to use within that application, which will download the appropriate language pack.
For Meta VR devices to correctly recognize your language pack you should name it with a language code per BCP47 format, with a suffix of “lang”. For example, en-us.lang and de.lang would be valid language pack names.
Upload a binary with language packs to the Meta Horizon Store
For language packs, use the --language_packs_dir parameter to specify the directory that contains the language packs.
When you upload new apps that have accompanying asset files, make sure the asset files have the same name as previously uploaded versions of the same file.
For Link PC-VR devices
Here is a sample command to upload a Rift package with a language pack:
In the top-right corner of the Meta Horizon Dashboard, select your org.
Select your app.
In the left-side navigation, select Distribution > Builds. A list of all builds for your app appears.
In the Build column, click on a build.
Select the Expansion Files tab.
Find the Expansion Files column for the build you selected and then select View Expansion Files. The different kinds of assets will display.
Look for Language Packs.
The following image shows an example of language packs.
Check for language packs in your app
Use the following steps to check for language packs in your app code.
The native asset file API hands you opaque handles. Read each field with an ovr_* accessor that takes the handle.
Use the ovr_AssetFile_GetList() function to get a list of all assets.
Check for an asset type via ovr_AssetDetails_GetAssetType() on each ovrAssetDetailsHandle. A language pack has the asset type language_pack.
Read the language of the asset from ovr_AssetDetails_GetLanguage(), which returns an ovrLanguagePackInfoHandle and returns NULL for an asset that carries no language info.
Download and apply a language pack at runtime
The runtime flow uses two language pack calls, get-current and set-current. Listing packs, tracking download progress, and confirming an install come from the asset file API rather than from a language pack API.
Work through the sections in order. Each one hands state to the next: get-current returns the tag that is applied now, progress observation has to be running before you apply a pack, set-current returns the asset ID that the status call takes, and the status response returns the filepath you load from.
Before your first request
Your app owns the message loop. Each call below returns immediately and delivers its result later as a message that you pop, dispatch, and free.
Run one loop that tests every message for an error, dispatches by message type, and frees the message on every path. None of the handlers in the sections below frees its message, so a handler can return early without leaking one.
#include <OVR_Platform.h>
// Call this once per frame. The cases are the ones this page uses.
void ProcessLanguagePackMessages(void) {
ovrMessageHandle message;
while ((message = ovr_PopMessage()) != NULL) {
if (!ovr_Message_IsError(message)) {
switch (ovr_Message_GetType(message)) {
case ovrMessage_LanguagePack_GetCurrent:
HandleCurrentLanguagePack(message);
break;
case ovrMessage_LanguagePack_SetCurrent:
HandleLanguagePackSet(message);
break;
case ovrMessage_AssetFile_StatusById:
HandleAssetStatus(message);
break;
case ovrMessage_AssetFile_GetList:
HandleAssetList(message);
break;
case ovrMessage_Notification_AssetFile_DownloadUpdate:
HandleDownloadUpdate(message);
break;
default:
break;
}
}
ovr_FreeMessage(message);
}
}
ovr_FreeMessage() destroys the payload handle and every string you read from it. A payload handle and its strings stay valid only until the loop frees the owning message, so copy anything you need after the handler returns.
Get the current language pack
The get-current call returns the asset details of the pack that is applied now. Read the BCP47 language tag from the language info, and read the on-disk location of the pack from the filepath.
Call ovr_LanguagePack_GetCurrent() and extract the payload with ovr_Message_GetAssetDetails(). ovr_AssetDetails_GetLanguage() returns NULL when the pack carries no language info, so check that handle as well before you read the tag.
// Send the request. ProcessLanguagePackMessages() dispatches the response.
ovr_LanguagePack_GetCurrent();
void HandleCurrentLanguagePack(ovrMessageHandle message) {
ovrAssetDetailsHandle details = ovr_Message_GetAssetDetails(message);
if (!details) { return; }
ovrLanguagePackInfoHandle info = ovr_AssetDetails_GetLanguage(details);
if (!info) { return; }
const char *tag = ovr_LanguagePackInfo_GetTag(info);
const char *path = ovr_AssetDetails_GetFilepath(details);
if (!tag || !path) { return; }
// Copy tag and path into your own storage here. Both become invalid
// when the message loop frees this message.
}
Track download progress
The asset file API reports download progress. Set up progress observation before you apply a pack, because an update that arrives before you are listening is lost.
Each update carries the bytes transferred and the total bytes. The bytes transferred value is -1 before the download starts, so check for a negative transferred count and a zero total before you compute a percentage.
Handle messages of type ovrMessage_Notification_AssetFile_DownloadUpdate and extract the payload with ovr_Message_GetAssetFileDownloadUpdate().
void HandleDownloadUpdate(ovrMessageHandle message) {
ovrAssetFileDownloadUpdateHandle update =
ovr_Message_GetAssetFileDownloadUpdate(message);
if (!update) { return; }
long long transferred =
ovr_AssetFileDownloadUpdate_GetBytesTransferredLong(update);
unsigned long long total =
ovr_AssetFileDownloadUpdate_GetBytesTotalLong(update);
if (transferred < 0 || total == 0) { return; }
float progress = (float)transferred / (float)total;
}
Use the Long accessors shown here. ovr_AssetFileDownloadUpdate_GetBytesTotal() and ovr_AssetFileDownloadUpdate_GetBytesTransferred() are deprecated.
Apply or switch a language pack
Pass the BCP47 tag of the pack to the set-current call. The call downloads the pack automatically when the pack is not installed on the device.
The tag carries no .lang suffix. A pack file named de.lang has the tag de.
The result of the call carries the asset ID of the pack. Store that asset ID in state your code owns, such as a field on the object that drives your picker, because the confirm step needs it after the callback returns. The result also carries a filepath. Do not read content from it here: the pack is ready only once the status call reports installed.
// Your own storage, not a local in the handler.
static ovrID g_languagePackAssetId = 0;
// Send the request. ProcessLanguagePackMessages() dispatches the response.
ovr_LanguagePack_SetCurrent("de");
void HandleLanguagePackSet(ovrMessageHandle message) {
ovrAssetFileDownloadResultHandle result =
ovr_Message_GetAssetFileDownloadResult(message);
if (!result) { return; }
g_languagePackAssetId = ovr_AssetFileDownloadResult_GetAssetId(result);
}
Confirm the install
A progress update whose completed flag is true means the download finished. It does not mean the pack is installed. Request the asset status repeatedly until the download status changes from available to installed, and read the filepath after that.
Your code owns that polling loop. Build it with all four of these:
A delay between requests, so the loop does not spin.
A bound, either a maximum number of attempts or a timeout, so a pack that never reaches installed does not hold the loop open.
A cancellation path, so leaving the picker ends the loop.
Error handling on every response, so a failed request retries or ends the loop rather than reading as a status that is not installed.
Each snippet below is the body of one iteration of that loop, not the whole loop.
One iteration, using the asset ID you stored from set-current:
#include <string.h>
// Send the request. ProcessLanguagePackMessages() dispatches the response.
ovr_AssetFile_StatusById(g_languagePackAssetId);
void HandleAssetStatus(ovrMessageHandle message) {
ovrAssetDetailsHandle details = ovr_Message_GetAssetDetails(message);
if (!details) { return; }
const char *status = ovr_AssetDetails_GetDownloadStatus(details);
if (!status || strcmp(status, "installed") != 0) { return; }
const char *path = ovr_AssetDetails_GetFilepath(details);
if (!path) { return; }
// Copy path into your own storage here. It becomes invalid when the
// message loop frees this message.
}
Load your localized content
Load your localized content from the filepath that the SDK returns. Never hardcode the path to a pack.
Take the filepath from the get-current response, or from a status response whose download status is installed. Do not load from the filepath on the set-current result, because at that point the pack is not confirmed installed.
Read the filepath from ovr_AssetDetails_GetFilepath() on the get-current or status payload.
Populate language picker entries
The samples in this section populate the entries of a picker: they list the packs and build one label per entry. They do not run the switch. Combine them with the sections above for the whole flow.
These are the six steps end to end, and the state each one hands to the next:
List every asset file, as described in Check for language packs in your app. Keep the entries whose asset type is the string language_pack. The list call takes no filter parameter, so filter the results in your app code. This gives you the entry list.
Label each entry from the names on its language info: the English name, for a list that stays readable to a user who selected the wrong language, and the native name, for the spelling used by that language. This gives you the display text.
Call get-current and pre-select the entry whose tag matches the tag it returns. This gives you the starting selection.
Start progress observation, then call set-current with the tag of the entry the user selects. Store the asset ID from the result in your picker state. This gives you the asset ID.
Request the asset status in your bounded loop until the download status is installed. Store the filepath from that response. This gives you the filepath.
Reload your localized content from the stored filepath, then end the progress observation.
#include <string.h>
// Send the request. ProcessLanguagePackMessages() dispatches the response.
ovr_AssetFile_GetList();
void HandleAssetList(ovrMessageHandle message) {
ovrAssetDetailsArrayHandle assets = ovr_Message_GetAssetDetailsArray(message);
if (!assets) { return; }
size_t count = ovr_AssetDetailsArray_GetSize(assets);
for (size_t i = 0; i < count; ++i) {
ovrAssetDetailsHandle details = ovr_AssetDetailsArray_GetElement(assets, i);
if (!details) { continue; }
const char *assetType = ovr_AssetDetails_GetAssetType(details);
if (!assetType || strcmp(assetType, "language_pack") != 0) { continue; }
ovrLanguagePackInfoHandle info = ovr_AssetDetails_GetLanguage(details);
if (!info) { continue; }
const char *english = ovr_LanguagePackInfo_GetEnglishName(info);
const char *native = ovr_LanguagePackInfo_GetNativeName(info);
if (!english || !native) { continue; }
// Copy english and native into your own storage here. Both become
// invalid when the message loop frees this message.
}
}
The native asset details array exposes no paging accessor, so there is no call to fetch a next page.