Skip to content
DedicatedPHP Contact

Internationalizing a PHP application without duplicating the domain

A guide to separating language, formats, and content from business rules when preparing a PHP application for different markets.

Editorial diagram of a PHP application separating language, regional formats, time zone, and business rules

Internationalizing a PHP application is not just about translating buttons and messages. An application prepared for multiple languages or markets must separate language, regional formats, time zone, currency, content, and business policies. If these layers are mixed, each expansion can become a functional fork that is difficult to test and maintain.

What changes when operating in multiple languages and markets

What changes when operating in multiple languages and markets — DedicatedPHP visual guide

Language determines how text is expressed. Regional settings, or locale, define presentation conventions such as the decimal separator, date order, or thousands grouping. A locale may include a region, such as es-ES or fr-CA, but that region must not replace the market, legal entity, residency, commercial policy, or operating country.

It is also useful to distinguish contexts that often travel together but do not mean the same thing:

  • Time zone: interprets schedules, deadlines, calendars, and operational cutoffs.
  • Currency: identifies the amount of a transaction or price list; it is not inferred from language.
  • Organization or legal entity: may determine taxes, permissions, invoicing, or data retention.
  • Market: may affect the catalog, logistics, payment methods, or available channels.
  • User preferences: selected language, locale, and time zone, which may differ from the corporate configuration.

A person may use the interface in English, work in a European time zone, and manage an organization that invoices in another currency. Reducing that reality to a single locale variable creates implicit decisions.

The costly mistake: turning language into a business rule

A bad sign appears when code makes domain decisions based on the interface language: if ($locale === 'es'). That conditional may start by displaying a different label and end up applying taxes, hiding a payment method, or changing an approval.

The right question is: “what data or policy explains this variation?” If it depends on a legal entity, that entity must be consulted. If it responds to a commercial policy, there must be an identifiable and versionable policy. If it only affects representation, it belongs at the input or output boundary.

Language translates the experience; it does not authorize, calculate, or define domain behavior by itself.

What must remain in the domain

The domain must work with stable concepts and canonical values. An order needs quantities, amounts, lines, statuses, and calculation rules; it does not need to know whether an amount will be displayed as 1,234.50 or 1.234,50. An eligibility policy must receive explicit attributes, not read the user's presentation settings.

  • Domain: invariants, statuses, calculations, business authorizations, policies, and events.
  • Application: use cases, context loading, coordination, and policy selection.
  • Input adapters: forms, headers, APIs, or files; validation and normalization.
  • Output adapters: translation, serialization, and formatting of dates, amounts, and units.

In PHP, avoid having entities and core services directly access the session, HTTP headers, environment variables, or the process-wide locale. These dependencies cause the same use case to behave differently depending on the channel or execution time.

Designing an explicit, limited context

An ExecutionContext may include the organization identifier, actor, presentation locale, and preferred time zone. Each field must have clear semantics. The currency of an operation, however, must be part of the amount or the applied pricing policy, not a mutable global preference.

Resolve context at the edge of each channel. A web request may use a saved preference or controlled negotiation; an API must receive explicit, documented fields; a queued process must persist the required identifiers when it is created. An asynchronous job must not assume it will inherit a session, user, or time zone.

Translatable content and operational data

Interface text, communication templates, and editorial content have a different lifecycle from operational data. Use stable, semantic keys, such as billing.invoice.overdue, rather than the original text. This allows wording to change without breaking code, tests, or integrations.

Managed content requires publication: a translation may exist as a draft, be approved, or be published. Define the fallback: requested language, the organization's base language, and, where appropriate, a visible and controlled absence. An alternative translation may be acceptable for an internal note, but not necessarily for a contractual communication.

Do not duplicate a complete operational record per language unless the data is localized. A product may have translatable names and descriptions, while its identifier, weight, status, and availability rules remain shared. If there is a real commercial difference, model it as a variant or policy, not as a translation.

Dates, amounts, units, and rounding

Store temporal instants unambiguously and retain the time zone when the meaning is local. “The meeting starts at 09:00” requires knowing the zone in which it was defined; “the event occurred at this time” requires an absolute instant. Daylight saving transitions create nonexistent or repeated hours, so input must be validated and the adopted resolution must be recorded where appropriate.

For money, store an integer amount in minor units together with the currency code, but do not assume two decimal places. The scale or exponent comes from the currency metadata applicable to the operation. Some currencies use a scale other than two, and historical requirements or those of a payment network may require retaining the effective scale or a version of the rule used.

final class Money {
    public function __construct(
        public readonly int $minorUnits,
        public readonly string $currency,
        public readonly int $scale
    ) {}
}

The scale makes it possible to interpret minor units correctly, but it does not replace a rounding policy. Define the rounding point, the applied mode, and the cash rule where one exists, since cash rounding may differ from accounting rounding. Avoid float, localized formats during calculation, and implicit conversions.

The same pattern applies to measurements: retain the original unit when it has operational meaning, normalize when calculation requires it, and convert only when capturing or presenting. A form must indicate the permitted unit and format; it must not guess whether 1,500 means one and a half or one thousand five hundred.

Input and output flow architecture

Layer separation must be visible in a complete, repeatable flow:

  1. Capture context and input data: obtain the organization, actor, channel, locale, time zone, and received value.
  2. Validate the permitted format: check required fields, syntax, unit, currency, time zone, and channel constraints.
  3. Normalize to canonical values: convert localized text into unambiguous amounts, dates, units, and identifiers.
  4. Execute the domain use case: apply explicit rules and policies to canonical values.
  5. Translate and format the output: select published messages and represent values for the recipient or API contract.

An API may choose to accept only canonical formats, such as dates with an explicit time zone and structured amounts. A human-facing form may accept localized formats, provided its parser is explicit. In both cases, the domain receives the same stable representation.

Configurable policy or distinct business rule

A variation is often configurable if it shares a process and changes declarable parameters, such as a limit, a holiday list, or a calculation method defined by configuration. That configuration needs a schema, version, owner, and tests.

The difference reveals a distinct rule when it changes invariants, states, responsibilities, data sources, or legal consequences. In that case, hiding it in options creates opaque configuration. Model a policy through an explicit interface, or a separate flow if the process is genuinely different. The goal is not to force a single abstraction, but to avoid duplicating the entire application because of a localized difference.

Testing and checklist

Testing and checklist — DedicatedPHP visual guide

Tests must verify calculation and representation. Changing the language or locale must not alter a total, an authorization, or a commercial policy, unless an explicit requirement establishes it.

  • Test dates across time changes, with ambiguous and nonexistent hours.
  • Cover zero and negative amounts, different scales, and accounting and cash rounding.
  • Check permitted localized input and rejection of ambiguous formats.
  • Verify fallback, absence of published content, and interpolated variables.
  • Run asynchronous jobs without a session, using only persisted context.
  • Test API contracts with canonical values and formatting metadata when necessary.

Before opening a language or market, identify what actually changes, separate its sources of truth, review monetary and temporal rules, and enable availability gradually when operations require controlled validation. This discipline makes it possible to expand the application without turning each market into a parallel version of the product.

Want to apply these ideas to your project?Let’s discuss your PHP platform.
View related service