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 Quest devices
On Quest, 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 Quest 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 Unreal asset file API hands you typed structs. Read each field directly on FOvrAssetDetails.
Use the OvrPlatform_AssetFile_GetList() function to get a list of all assets.
Check for an asset type via the FOvrAssetDetails::AssetType field. A language pack has the asset type language_pack.
Read the language of the asset from the FOvrAssetDetails::Language struct, whose Tag field holds the BCP47 language code.
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
Each request function takes a UGameInstance*. Hold a game instance and test it with IsValid() before you call one.
Notifications come from UOvrPlatformSubsystem, which you fetch from that same game instance with GetSubsystem<UOvrPlatformSubsystem>(). Test the returned pointer before you bind a handler or start the message pump.
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 OvrPlatform_LanguagePack_GetCurrent() with your UGameInstance. The first delegate parameter is the success flag. On failure the payload pointer is still non-null, so test the bool and not the pointer. FOvrAssetDetails::Language is a struct value rather than a pointer, so check Tag.IsEmpty() instead of checking for null.
// GameInstance is your UGameInstance*.
if (!IsValid(GameInstance)) { return; }
OvrPlatform_LanguagePack_GetCurrent(GameInstance,
OvrPlatform_LanguagePack_GetCurrent_Delegate::CreateLambda(
[](bool bIsSuccessful, FOvrAssetDetailsPtr Details, FString ErrorMsg)
{
if (!bIsSuccessful || !Details.IsValid()) { return; }
if (Details->Language.Tag.IsEmpty()) { return; }
const FString Tag = Details->Language.Tag;
const FString Path = Details->Filepath;
}));
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.
Bind UOvrPlatformSubsystem::OnAssetFileDownloadUpdate, then call StartMessagePump(). The message pump stays inert until you call StartMessagePump(), and no notification arrives before that call.
Declare the handler in your UMyPicker class header. The UFUNCTION() macro is required because AddDynamic resolves the handler by name at runtime.
Read AssetId on the update. AssetFileId is 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.
// LanguagePackAssetId is an FOvrId member on your picker class.
if (!IsValid(GameInstance)) { return; }
OvrPlatform_LanguagePack_SetCurrent(GameInstance, TEXT("de"),
OvrPlatform_LanguagePack_SetCurrent_Delegate::CreateLambda(
[this](bool bIsSuccessful, FOvrAssetFileDownloadResultPtr Result, FString ErrorMsg)
{
if (!bIsSuccessful || !Result.IsValid()) { return; }
LanguagePackAssetId = Result->AssetId;
}));
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:
// InstalledPath is an FString member on your picker class.
if (!IsValid(GameInstance)) { return; }
OvrPlatform_AssetFile_StatusById(GameInstance, LanguagePackAssetId,
OvrPlatform_AssetFile_StatusById_Delegate::CreateLambda(
[this](bool bIsSuccessful, FOvrAssetDetailsPtr Details, FString ErrorMsg)
{
if (!bIsSuccessful || !Details.IsValid()) { return; }
if (Details->DownloadStatus == TEXT("installed"))
{
InstalledPath = Details->Filepath;
}
}));
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 FOvrAssetDetails::Filepath on the get-current or status result.
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.
// GameInstance is your UGameInstance*.
if (!IsValid(GameInstance)) { return; }
OvrPlatform_AssetFile_GetList(GameInstance,
OvrPlatform_AssetFile_GetList_Delegate::CreateLambda(
[](bool bIsSuccessful, FOvrAssetDetailsArrayPtr Assets, FString ErrorMsg)
{
if (!bIsSuccessful || !Assets.IsValid()) { return; }
for (const FOvrAssetDetails& Asset : *Assets)
{
if (Asset.AssetType != TEXT("language_pack") || Asset.Language.Tag.IsEmpty()) { continue; }
const FString Label = Asset.Language.EnglishName + TEXT(" (") + Asset.Language.NativeName + TEXT(")");
}
}));