Our portals run in Slovakia, the Czech Republic and Switzerland: four portals in three markets, three languages, three currencies, two database systems and one shared architecture. This article is about the three repositories, one per market. It is written for teams that run a product in several countries, or are about to, and are weighing one codebase with a country switch against separate deployments. It describes how we decided, what it costs us and where we made mistakes.

Why three repositories rather than one codebase

One codebase with a country setting is cheaper as long as the markets differ only in language and content. Ours differ more, and four reasons settled it:

  • Regulation and legal texts. Each country has its own rules on content, mandatory notices and data protection. These are written for the country, not translated.
  • Hosting. Two markets run on PostgreSQL with one provider, the third on MySQL with another, with different limits. A single codebase would have to carry exceptions for both environments.
  • Pace. New features usually appear in one market first and move to others when they make sense there. Some data features and entire editorial modules exist in only one market today.
  • Regression risk. A change deployed in one market does not touch the others until someone decides to carry it over. A code bug in one market does not take down three sites at once.

The cost is that repositories drift apart over time and every shared change has to be carried over explicitly. The Swiss repository, for example, keeps articles in a separate folder and builds review pages from data files, while the other two keep them as individual pages. If your markets shared hosting, release rhythm and legal framework, choose one codebase.

What is shared and what is local

The shared elements are architectural principles: plain PHP without a framework, the same database library at the same version in all three repositories, the same catalogue data model, credentials and settings kept outside the repository, the same URL routing pattern and the same kind of definition-of-done checklist for every page. Everything a reader or a regulator sees is local: content, page URLs, the list of partners and their redirect links, currency, number formats, legal texts, cookie consent settings and the editorial rules for each language.

The boundary is written down in each repository’s documentation. It has one explicit exception: operational runbooks that cover all three projects at once (database access or visual checks, for instance) exist as three identical copies and are changed one to one. They are the only kind of content copied between repositories.

Carrying a change over: intent, not files

When a feature or a fix lands in one market, its purpose travels to the others. The procedure is written down and is the same for every change:

  • Find the nearest functional equivalent in the target project: the file, template or feature that does the same job.
  • Keep the purpose of the change and adapt texts, URLs, internal links, partner names, currency and images.
  • Do not copy blindly where the target project has a different structure.
  • If there is no direct equivalent, choose the nearest place and state that assumption explicitly when handing the change over.
  • Name whatever is not carried to a given market, so it does not look like forgotten work.

The typical mistake is to assume that the URL is the same. In our case the language alternates of thirteen pages pointed to Czech URLs that did not exist, because Czech URLs take different forms. More copying would not have helped. The fix was an explicit map of equivalents, and where the map has no equivalent, no link is output. The carry-over rules are written so that a person and an AI coding assistant apply them in the same way, and having them in writing is what makes them a process.

Two databases: what abstraction solves and what it does not

The Slovak and Czech markets share one PostgreSQL database and differ by schema, while the Swiss market runs on MySQL with a different provider. In the code the schema name is always a variable read from configuration, never text inside a query, so the same query serves both markets in the shared database. A database library with named placeholders for identifiers and values adapts quoting and escaping to each system.

Abstraction does not turn two databases into one, though. Shared catalogue queries are written to work on both systems. Features that rely on PostgreSQL-specific capabilities, such as JSON data types, exist only in the PostgreSQL markets, and carrying them to MySQL would mean rewriting the queries, not copying them. We handle the difference through feature scope instead of a layer that hides it.

Hosting limits matter just as much. On one test database, the provider caps the number of new connections per hour, and every page view opens a connection. A bulk check tuned on another market can then report database connection errors rather than code errors, so it runs in batches. Check the limits separately for test and production, because a limit found on one does not prove that the other has it.

One admin for all markets

The internal admin exists as a single instance in one repository and serves all the portals as well as the company website. One version to maintain and one security perimeter matter more than the convenience of having a tool in every repository. It runs sitemap generation, checks for broken and partner links and bulk HTML validation for all sites. Catalogue items are exported from the Slovak data to the Czech data with a currency conversion that is only a proposal: values are rounded up to whole crowns, the description is translated and a person reviews both before anything is written.

The editorial modules cover only one market so far, and data for the others is changed through SQL scripts. For the future, the rule is that when a market lacks a process, it is built as a new module in the shared admin, not as a tool in that market’s repository. Admin changes are therefore not synchronised between markets, as there is nothing to synchronise.

Localisation is not translation

Translation deals with language. Localisation deals with everything else that has to be right in a market:

  • Currency. Values that change often come from configuration or the database, never hard-coded in article text. Otherwise they go out of date before anyone notices.
  • Number formats. In Slovakia and the Czech Republic a space between thousands and a decimal comma; in Switzerland an apostrophe and a decimal point. The CLDR standard uses a typographic apostrophe and many texts a straight one. What matters is choosing one and sticking to it.
  • Spelling. Swiss Standard German does not use ß and writes a double s instead. Editorial rules are therefore separate for each language, not translated.
  • Legal texts and cookie consent. Written for the country and its regulations, not translated from another market.
  • Partners. Each market has its own list of partners and its own redirect links. Carrying a feature over never takes them across automatically.
  • URLs. The same feature has a different URL in the local language in each market, so it needs a map of equivalents.

Hreflang across domains: only for a genuine equivalent

Our portals sit on different domains. Google allows language alternates across domains, but each version must list itself and all the others and the links must be reciprocal, otherwise the annotations are ignored. Hreflang is meant for language or regional versions of the same page. According to its own documentation, Google does not use it to determine a page’s language.

That is why we use it between markets only on pages that are a localised version of the same content, such as home and overview pages, the methodology page or selected guides. Most pages list only themselves and the default version, because a Czech or Swiss article on a similar topic is a different article, not a translation. The canonical URL always points to the page in its own language, never to another market. The errors our own audits found were inconsistent language codes between portals, alternates pointing to non-existent or redirected URLs, and a block of tags hand-written into the head of every page, which is hard to change in bulk.

  • Hreflang only for a genuine equivalent. A similar topic is not enough.
  • Consistent language and region codes across all portals in the group.
  • One map of equivalents instead of hand-written tags in every page head.
  • When a URL changes or disappears in one market, check the links from the other markets.
  • A canonical URL in the same language as the page.

Recommendation

Write down what is shared and what is local first, and only then decide how many repositories you need. If markets differ in regulation, hosting or pace, separate deployments with a shared architecture and a written carry-over procedure are safer than one codebase full of exceptions. Do not plan the same feature set for every market, because each carry-over is a decision for a specific market. A small team can sustain this because most of the carry-over work is deciding, not rewriting.