Skip to content

Phone Number Utilities with libphonenumber in PHP: Country, Language, and Timezone Detection

Introduction

When building global applications, phone numbers are often the most reliable piece of user data available. Unlike email addresses or usernames, phone numbers carry inherent geographic information that can help personalize user experiences. From automatically selecting the right language for notifications to scheduling messages at appropriate local times, extracting metadata from phone numbers is invaluable.

This guide demonstrates how to build a PhoneUtil class using Google's libphonenumber library (via the PHP port) to extract country codes, infer preferred languages, and detect timezones from international phone numbers.

Prerequisites

  • PHP 8.1 or higher
  • Composer package manager
  • Basic understanding of PHP classes and exception handling

Installing libphonenumber-for-php

Install the library via Composer:

bash
composer require giggsey/libphonenumber-for-php

Lite Version Available

If you only need basic parsing and formatting without geocoding or carrier information, you can use giggsey/libphonenumber-for-php-lite for a smaller footprint.

Building the PhoneUtil Class

Let's create a utility class that provides three key methods: country code detection, language inference, and timezone resolution.

Basic Structure

php
<?php

namespace App\Utils;

use libphonenumber\PhoneNumberToTimeZonesMapper;
use libphonenumber\PhoneNumberUtil;
use Throwable;

class PhoneUtil
{
    // Methods will go here
}

Getting the Country Code

The first method extracts the ISO 3166-1 alpha-2 country code (e.g., "US", "BR", "MX") from a phone number:

php
public static function getCountryCode(string $phone_number): ?string
{
    try {
        $phoneUtil = PhoneNumberUtil::getInstance();
        $phoneObj = $phoneUtil->parse("+{$phone_number}");

        return $phoneUtil->getRegionCodeForNumber($phoneObj);
    } catch (Throwable $e) {
        // Log or report the error
        return null;
    }
}

Phone Number Format

This implementation expects phone numbers without the leading + sign. The method prepends it during parsing. Adjust based on how your application stores phone numbers.

Inferring User Language

Based on the country code, we can make educated guesses about a user's preferred language. This is particularly useful for sending localized notifications:

php
public static function getCountryLanguage(string $phone_number): ?string
{
    try {
        $country_code = self::getCountryCode($phone_number);

        if (! $country_code) {
            return null;
        }

        return match ($country_code) {
            // Spanish-speaking countries
            'ES', 'MX', 'AR', 'CO', 'PE', 'VE', 'CL', 'EC',
            'GT', 'CU', 'BO', 'DO', 'HN', 'PY', 'SV', 'NI',
            'CR', 'PA', 'UY' => 'es',

            // Portuguese
            'BR' => 'pt_BR',

            // Chinese
            'CN', 'TW', 'HK', 'SG' => 'zh_CN',

            // Hindi
            'IN' => 'hi',

            // Default to English
            default => 'en',
        };
    } catch (Throwable $e) {
        return null;
    }
}

Expanding Language Support

The mapping above covers major languages. Extend it based on your application's supported locales. Consider countries like France (FR => fr), Germany (DE => de), or Japan (JP => ja) as needed.

Detecting Timezone

Timezone detection is crucial for scheduling user communications at appropriate times. The libphonenumber library provides the PhoneNumberToTimeZonesMapper class for this purpose:

php
public static function getTimezone(string $phone_number): ?string
{
    try {
        $country_code = self::getCountryCode($phone_number);
        $phoneUtil = PhoneNumberUtil::getInstance();
        $phoneObj = $phoneUtil->parse("+{$phone_number}", $country_code);
        $timezoneMapper = PhoneNumberToTimeZonesMapper::getInstance();
        $timezones = $timezoneMapper->getTimeZonesForNumber($phoneObj);
        $timezone = $timezones[0] ?? null;

        if (! $timezone) {
            return null;
        }

        // Handle unknown timezone fallbacks
        if ($timezone === 'Etc/Unknown') {
            return match ($country_code) {
                'MX' => 'America/Mexico_City',
                'BR' => 'America/Sao_Paulo',
                'IN' => 'Asia/Kolkata',
                default => null,
            };
        }

        return $timezone;
    } catch (Throwable $e) {
        return null;
    }
}

Handling Deprecated Timezones

Some phone numbers may return deprecated timezone identifiers. It's good practice to map these to their modern equivalents:

php
public static function getTimezone(string $phone_number): ?string
{
    try {
        // ... previous code ...

        // Map deprecated timezones to modern equivalents
        $deprecatedTimezones = [
            'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
            'America/Catamarca' => 'America/Argentina/Catamarca',
            'America/Cordoba' => 'America/Argentina/Cordoba',
            'America/Jujuy' => 'America/Argentina/Jujuy',
            'America/Mendoza' => 'America/Argentina/Mendoza',
        ];

        if (isset($deprecatedTimezones[$timezone])) {
            return $deprecatedTimezones[$timezone];
        }

        return $timezone;
    } catch (Throwable $e) {
        return null;
    }
}

Complete Implementation

Here's the full PhoneUtil class combining all the methods:

php
<?php

namespace App\Utils;

use libphonenumber\PhoneNumberToTimeZonesMapper;
use libphonenumber\PhoneNumberUtil;
use Throwable;

class PhoneUtil
{
    public static function getCountryCode(string $phone_number): ?string
    {
        try {
            $phoneUtil = PhoneNumberUtil::getInstance();
            $phoneObj = $phoneUtil->parse("+{$phone_number}");

            return $phoneUtil->getRegionCodeForNumber($phoneObj);
        } catch (Throwable $e) {
            // Consider logging: Errors::reportThrowable($e);
            return null;
        }
    }

    public static function getCountryLanguage(string $phone_number): ?string
    {
        try {
            $country_code = self::getCountryCode($phone_number);

            if (! $country_code) {
                return null;
            }

            return match ($country_code) {
                'ES', 'MX', 'AR', 'CO', 'PE', 'VE', 'CL', 'EC',
                'GT', 'CU', 'BO', 'DO', 'HN', 'PY', 'SV', 'NI',
                'CR', 'PA', 'UY' => 'es',
                'BR' => 'pt_BR',
                'CN', 'TW', 'HK', 'SG' => 'zh_CN',
                'IN' => 'hi',
                default => 'en',
            };
        } catch (Throwable $e) {
            return null;
        }
    }

    public static function getTimezone(string $phone_number): ?string
    {
        try {
            $country_code = self::getCountryCode($phone_number);
            $phoneUtil = PhoneNumberUtil::getInstance();
            $phoneObj = $phoneUtil->parse("+{$phone_number}", $country_code);
            $timezoneMapper = PhoneNumberToTimeZonesMapper::getInstance();
            $timezones = $timezoneMapper->getTimeZonesForNumber($phoneObj);
            $timezone = $timezones[0] ?? null;

            if (! $timezone) {
                return null;
            }

            if ($timezone === 'Etc/Unknown') {
                return match ($country_code) {
                    'MX' => 'America/Mexico_City',
                    'BR' => 'America/Sao_Paulo',
                    'IN' => 'Asia/Kolkata',
                    default => null,
                };
            }

            $deprecatedTimezones = [
                'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
                'America/Catamarca' => 'America/Argentina/Catamarca',
                'America/Cordoba' => 'America/Argentina/Cordoba',
                'America/Jujuy' => 'America/Argentina/Jujuy',
                'America/Mendoza' => 'America/Argentina/Mendoza',
            ];

            return $deprecatedTimezones[$timezone] ?? $timezone;
        } catch (Throwable $e) {
            return null;
        }
    }
}

Usage Examples

Basic Usage

php
use App\Utils\PhoneUtil;

// Brazilian phone number
$phone = '5511999998888';

$country = PhoneUtil::getCountryCode($phone);      // "BR"
$language = PhoneUtil::getCountryLanguage($phone); // "pt_BR"
$timezone = PhoneUtil::getTimezone($phone);        // "America/Sao_Paulo"

Practical Application: Localized Notifications

php
public function sendNotification(User $user, string $message): void
{
    $language = PhoneUtil::getCountryLanguage($user->phone);
    $timezone = PhoneUtil::getTimezone($user->phone);

    // Set locale for translation
    app()->setLocale($language ?? 'en');

    // Schedule at appropriate local time
    $sendAt = now($timezone)->setTime(10, 0);

    Notification::send($user, new UserNotification(
        message: __($message),
        scheduledAt: $sendAt
    ));
}

Debugging Phone Numbers

The libphonenumber project provides an online demo for testing phone number parsing:

https://libphonenumber.appspot.com

Use this tool to verify expected behavior before reporting issues.

Troubleshooting

Unknown Timezone Returns

For some mobile numbers, especially in large countries like Brazil, Mexico, or India, the library may return Etc/Unknown. This happens when the number doesn't provide enough geographic specificity. Handle this with country-level fallbacks as shown above.

Parsing Failures

Common causes of parsing failures:

  • Invalid phone number format
  • Missing or incorrect country code prefix
  • Non-numeric characters in the input

Always sanitize phone numbers before processing:

php
$phone = preg_replace('/[^0-9]/', '', $phone);

Conclusion

Building phone number utilities with libphonenumber provides a reliable foundation for internationalized applications. By extracting country codes, inferring languages, and detecting timezones, you can deliver personalized experiences that respect users' geographic contexts.

The key takeaways are:

  • Use PhoneNumberUtil for parsing and country detection
  • Map country codes to your supported locales for language inference
  • Handle Etc/Unknown timezones with sensible country-level defaults
  • Map deprecated timezone identifiers to their modern equivalents

For applications with strict requirements, consider allowing users to override inferred values in their preferences while using phone-based detection as intelligent defaults.