Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions config/nativephp.php
Original file line number Diff line number Diff line change
Expand Up @@ -375,4 +375,21 @@
'landscape_right' => false,
],
],

/*
|--------------------------------------------------------------------------
| Supported Locales
|--------------------------------------------------------------------------
|
| The locales your app supports (e.g. ['en', 'fr', 'es', 'de']). When
| two or more locales are listed, iOS will resolve Locale.current using
| the device's preferred language (via CFBundleLocalizations in
| Info.plist) and Android 13+ will show a per-app language picker
| (via locales_config.xml).
|
| Leave empty to skip locale configuration entirely.
|
*/

'locales' => [],
];
71 changes: 71 additions & 0 deletions src/Commands/BuildIosAppCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ private function configureXcodeProject(): bool
$this->updateBuildNumber();
$this->setAppName();
$this->updateInfoPlistFiles();
$this->updateLocalizationConfig();
$this->configureDeviceOrientations();
$this->updateEntitlementsFile();
$this->configureProvisioningProfile();
Expand Down Expand Up @@ -313,6 +314,76 @@ private function updateInfoPlistFiles(): void
});
}

/**
* Inject CFBundleLocalizations into Info.plist files when two or more locales are configured.
*/
private function updateLocalizationConfig(): void
{
$locales = config('nativephp.locales', []);

if (count($locales) < 2) {
return;
}

$plistFiles = [
$this->containerPath.'Info.plist',
$this->basePath.'/NativePHP-simulator-Info.plist',
];

foreach ($plistFiles as $filePath) {
if (! file_exists($filePath)) {
continue;
}

$this->injectCFBundleLocalizations($filePath, $locales);
}
}

private function injectCFBundleLocalizations(string $filePath, array $locales): void
{
$dom = new \DOMDocument;
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;

if (! $dom->load($filePath)) {
return;
}

$rootDict = $dom->getElementsByTagName('dict')->item(0);
if (! $rootDict) {
return;
}

$plistData = $this->parsePlistDict($rootDict);

if (isset($plistData['CFBundleLocalizations'])) {
$arrayNode = $plistData['CFBundleLocalizations']['valueNode'];
while ($arrayNode->firstChild) {
$arrayNode->removeChild($arrayNode->firstChild);
}
} else {
$rootDict->appendChild($dom->createElement('key', 'CFBundleLocalizations'));
$arrayNode = $dom->createElement('array');
$rootDict->appendChild($arrayNode);
}

foreach ($locales as $locale) {
$arrayNode->appendChild($dom->createElement('string', $locale));
}

$xmlContent = $dom->saveXML();

if (! str_contains($xmlContent, '<!DOCTYPE')) {
$xmlContent = preg_replace(
'/<\?xml.*?\?>\s*/s',
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n",
$xmlContent
);
}

file_put_contents($filePath, $xmlContent);
}

/**
* Configure device orientations and targeting by updating the Xcode project settings directly.
* Sets TARGETED_DEVICE_FAMILY and INFOPLIST_KEY_UISupportedInterfaceOrientations in project.pbxproj.
Expand Down
15 changes: 15 additions & 0 deletions src/Data/Localization.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Native\Mobile\Data;

class Localization
{
public function __construct(
public readonly string $locale,
public readonly string $languageCode,
public readonly string $regionCode,
public readonly string $timezone,
public readonly string $currencyCode,
public readonly string $preferredLanguage,
) {}
}
24 changes: 24 additions & 0 deletions src/Device.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Native\Mobile;

use Native\Mobile\Data\Localization;

class Device
{
public function getId(): ?string
Expand Down Expand Up @@ -46,6 +48,28 @@ public function getBatteryInfo(): ?string
return null;
}

public function localization(): ?Localization
{
if (function_exists('nativephp_call')) {
$result = nativephp_call('Device.GetLocale', '{}');
if ($result) {
$decoded = json_decode($result, true);
$info = json_decode($decoded['info'] ?? '{}', true);

return new Localization(
locale: $info['locale'] ?? '',
languageCode: $info['languageCode'] ?? '',
regionCode: $info['regionCode'] ?? '',
timezone: $info['timezone'] ?? '',
currencyCode: $info['currencyCode'] ?? '',
preferredLanguage: $info['preferredLanguage'] ?? '',
);
}
}

return null;
}

/**
* Vibrate the device with a short haptic feedback.
*
Expand Down
1 change: 1 addition & 0 deletions src/Facades/Device.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @method static string|null getId()
* @method static string|null getInfo()
* @method static string|null getBatteryInfo()
* @method static \Native\Mobile\Data\Localization|null localization()
* @method static bool vibrate()
* @method static array toggleFlashlight()
*/
Expand Down
57 changes: 57 additions & 0 deletions src/Traits/PreparesBuild.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ protected function updateAndroidConfiguration(): void
$this->logToFile(' Updating orientation configuration...');
$this->updateOrientationConfiguration();

$this->logToFile(' Updating localization configuration...');
$this->updateLocalizationConfiguration();

$scheme = config('nativephp.deeplink_scheme');
$host = config('nativephp.deeplink_host');
$this->logToFile(' Updating deep link configuration...');
Expand Down Expand Up @@ -854,6 +857,60 @@ protected function updateBuildConfiguration(): void
File::put($proguardPath, $proguardContent);
}

/**
* Generate locales_config.xml and update AndroidManifest.xml when two or more locales are configured.
*/
protected function updateLocalizationConfiguration(): void
{
$locales = config('nativephp.locales', []);
$manifestPath = base_path('nativephp/android/app/src/main/AndroidManifest.xml');
$xmlDir = base_path('nativephp/android/app/src/main/res/xml');
$localesConfigPath = $xmlDir.'/locales_config.xml';

if (count($locales) < 2) {
// Clean up if locales were previously configured but now removed
if (File::exists($localesConfigPath)) {
File::delete($localesConfigPath);
}

if (File::exists($manifestPath)) {
$contents = File::get($manifestPath);
$cleaned = preg_replace('/\s*android:localeConfig="@xml\/locales_config"/', '', $contents);
if ($contents !== $cleaned) {
File::put($manifestPath, $this->normalizeLineEndings($cleaned));
}
}

return;
}

// Generate locales_config.xml
File::ensureDirectoryExists($xmlDir);

$xml = '<?xml version="1.0" encoding="utf-8"?>'."\n";
$xml .= '<locale-config xmlns:android="http://schemas.android.com/apk/res/android">'."\n";
foreach ($locales as $locale) {
$xml .= ' <locale android:name="'.htmlspecialchars($locale, ENT_XML1).'"/>'."\n";
}
$xml .= '</locale-config>'."\n";

File::put($localesConfigPath, $xml);

// Add android:localeConfig to <application> tag in manifest
if (File::exists($manifestPath)) {
$contents = File::get($manifestPath);

if (! str_contains($contents, 'android:localeConfig')) {
$contents = preg_replace(
'/(<application\b)/',
'$1 android:localeConfig="@xml/locales_config"',
$contents
);
File::put($manifestPath, $this->normalizeLineEndings($contents));
}
}
}

/**
* Replace the placeholder package name in all Kotlin files
* This is used to update plugin files that use com.example.androidphp as a placeholder
Expand Down
55 changes: 55 additions & 0 deletions tests/Concerns/MocksPreparesBuildDependencies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Tests\Concerns;

use Illuminate\Support\Facades\File;

/**
* Stubs for the abstract and undeclared methods required by PreparesBuild.
*/
trait MocksPreparesBuildDependencies
{
protected function info($message) {}

protected function warn($message) {}

protected function error($message) {}

protected function line($message) {}

protected function newLine() {}

protected function logToFile(string $message): void {}

protected function removeDirectory(string $path): void
{
if (is_dir($path)) {
File::deleteDirectory($path);
}
}

protected function platformOptimizedCopy(string $source, string $destination, array $excludedDirs): void {}

protected function detectCurrentAppId(): ?string
{
return null;
}

protected function updateAppId(string $oldAppId, string $newAppId): void {}

protected function updateLocalProperties(): void {}

protected function updateVersionConfiguration(): void {}

protected function updateAppDisplayName(): void {}

protected function updateDeepLinkConfiguration(): void {}

protected function updatePermissions(): void {}

protected function updateIcuConfiguration(): void {}

protected function updateFirebaseConfiguration(): void {}

protected function updateOrientationConfiguration(): void {}
}
Loading
Loading