Seedfast

Seedfast

Localized Test Data in Arabic, German and a Three-Country Run

Localized test data raises two questions worth checking separately, whether values are in the language you asked for and whether they are shaped the way that locale shapes them. The runs below used the schema whose DDL is printed in full on scope examples; most of the rows quoted here sit in crm.customers, crm.contacts and crm.addresses, and where a sentence mentioned invoices the billing tables came in as well. Each scope ran three times with the released CLI 2.6.3 against a fresh database holding nothing but that schema, twice with --output plain and once with --output json, each seed followed by the same verification queries.

Writing the scope in a language other than English

The scope is read as an instruction, not matched against a keyword list, a claim data realism makes in a paragraph and this pair of runs checks. One sentence went in twice, in the --scope form scoping describes.

seedfast seed --scope "zasej 50 zákazníkov s kontaktmi a faktúrami" --output plain
seedfast seed --scope "seed 50 customers with contacts and invoices" --output plain

Both resolved to the same five tables, and the transcripts below are trimmed to three lines each, Slovak first.

[2026-09-06T18:09:19+02:00] INFO: Tables in scope: crm.customers, crm.contacts, billing.plans, billing.subscriptions, billing.invoices
[2026-09-06T18:09:19+02:00] INFO: Planned: 303 records across 5 tables
[2026-09-06T18:09:57+02:00] INFO: Seeding completed: 5/5 tables succeeded, 303 rows, 78.69s
[2026-09-06T17:16:03+02:00] INFO: Tables in scope: crm.customers, crm.contacts, billing.plans, billing.subscriptions, billing.invoices
[2026-09-06T17:16:03+02:00] INFO: Planned: 205 records across 5 tables
[2026-09-06T17:16:52+02:00] INFO: Seeding completed: 5/5 tables succeeded, 205 rows, 84.60s

Six runs, three per language, put the same five tables in scope and gave the customers table exactly 50 rows. The tables named without a number moved inside one band, 50 or 100 contacts, 3 or 5 plans, 50 or 100 invoices.

Write the scope in Slovak and you have set the language of the instruction, which is a separate setting from the locale of the values, so name the locale you want in a clause of its own.

Arabic script names and Latin emails on the company domain

The Arabic test data below came from one four-sentence scope, two clauses of it negative.

seedfast seed --scope "150 customers, each with 2 contacts. Company names and contact names in Arabic script. Contact emails in Latin script on each company's own domain. No phone numbers and no addresses." --output plain

Run 1 finished in 68 seconds, trimmed here to the plan and two tables.

[2026-09-06T17:37:55+02:00] INFO: Tables in scope: crm.contacts, crm.customers
[2026-09-06T17:37:55+02:00] INFO: Planned: 450 records across 2 tables
[2026-09-06T17:38:19+02:00] INFO: Table crm.customers completed: 150 rows in 10.267s
[2026-09-06T17:38:34+02:00] INFO: Table crm.contacts completed: 300 rows in 14.644s
[2026-09-06T17:38:34+02:00] INFO: Seeding completed: 2/2 tables succeeded, 450 rows, 68.03s

Behind that 300 sits 2 contacts for each of the 150 customers, a per parent count the other two runs repeated.

The script check counts name columns against Unicode ranges, and the block below is trimmed to the script columns.

=== SCRIPT CHECKS (names) ===
 contacts | arabic_script | japanese_script | latin_only | mixed_arabic_latin
----------+---------------+-----------------+------------+--------------------
      300 |           300 |               0 |          0 |                  0

 customers | arabic_script_names | japanese_script_names
-----------+---------------------+-----------------------
       150 |                 150 |                     0

300 of 300 contact names in Arabic script, none carrying a Latin character, and 150 of 150 company names the same, numbers that repeated in all three runs. So did the email clause, checked by comparing the part after the @ with the customer's domain column.

=== EMAIL DOMAIN MATCH (contact email on the company domain) ===
 contacts | email_on_company_domain
----------+-------------------------
      300 |                     300

Both negatives held to the row in all three runs, 0 rows in crm.addresses and 0 phones. Job titles went unmentioned in that sentence, and run 1 returned them in Arabic beside the names, four rows of which follow, trimmed to the name and title columns.

   full_name   |      job_title
---------------+---------------------
 أحمد العتيبي  | مدير المبيعات
 محمد القحطاني | مسؤول العمليات
 علي المالكي   | مدير التسويق
 عمر النجدي    | منسق الحسابات

Where the emails have to sit on something that reads like a registered company, the domain column takes a clause of its own. A second scope repeated those four sentences with that addition, that the domains look like real company domains rather than placeholders, and run 3 of it returned the two rows below, trimmed to the name and domain columns.

          name          |      domain
------------------------+-------------------
 شركة النور التقنية     | alnoortech.com
 مؤسسة الازدهار التقنية | alizdihartech.com

German names, +49 phones and five-digit postal codes

One sentence set the language of the names, the phone prefix, the street line and the postal code format.

seedfast seed --scope "100 customers based in Germany, each with one billing address and 2 contacts. German company names, German contact names, phone numbers on +49, street lines and five-digit postal codes shaped the way German addresses are shaped." --output plain

The format check counts rows against a pattern. It is trimmed below to four of its ten columns, the Spanish and Japanese ones being 0 here.

=== FORMAT CHECKS ===
 de_addresses | de_postal_5_digits | contacts_with_phone | phone_plus49
--------------+--------------------+---------------------+--------------
          100 |                100 |                 200 |          200

Those four numbers repeated in all three runs, alongside 100 addresses of kind billing and country code DE. Underneath those totals, and again in each of the three runs, the per customer counts came back at 2 contacts and 1 billing address. Run 1 produced the German test data below, the contacts trimmed to four rows and to the name and title columns.

    full_name    |     job_title
-----------------+--------------------
 Lukas Müller    | Geschäftsführer
 Moritz Koch     | Einkaufsleiter
 Anna Hartmann   | Vertriebsleiter
 Katharina Weber | Leiter Finanzen

The run's first six billing addresses by id sit on German street names in six different cities.

    street_line    |       city        |       region        | postal_code | country_code
-------------------+-------------------+---------------------+-------------+--------------
 Hauptstraße 1     | Berlin            | Berlin              | 10115       | DE
 Bahnhofstraße 8   | Hamburg           | Hamburg             | 20095       | DE
 Gartenstraße 15   | München           | Bayern              | 80331       | DE
 Goethestraße 22   | Köln              | Nordrhein-Westfalen | 50667       | DE
 Schillerstraße 29 | Frankfurt am Main | Hessen              | 60311       | DE
 Kaiserstraße 36   | Stuttgart         | Baden-Württemberg   | 70173       | DE

In all three runs the same city and postal code pairs turned up, on German street names, with German legal forms on the company names (GmbH, AG, KG and GmbH & Co. KG in run 1) and German job titles.

Multilingual test data across three countries

The last scope splits a single run across three countries by percentage.

seedfast seed --scope "200 customers, each with one billing address and one contact: 50% in Germany, 30% in Spain, 20% in Japan. Company names, contact names, street addresses, postal codes and phone numbers local to each customer's country." --output plain

Across the three runs 200 customers came back with one contact and one billing address apiece. Runs 1 and 3 landed exactly on 100, 60 and 40 addresses, the behaviour named shares show on exact row counts.

=== ADDRESS COUNTRIES ===
 country_code |  n  | pct
--------------+-----+------
 DE           | 100 | 50.0
 ES           |  60 | 30.0
 JP           |  40 | 20.0

Postal formats matched the country on 100% of addresses in all three runs, and run 1 is below, trimmed to the six postal columns.

 de_addresses | de_postal_5_digits | es_addresses | es_postal_5_digits | jp_addresses | jp_postal_nnn_nnnn
--------------+--------------------+--------------+--------------------+--------------+--------------------
          100 |                100 |           60 |                 60 |           40 |                 40

Phone country codes, counted the same way, lined up with the address split in run 1, 100 on +49, 60 on +34 and 40 on +81.

The two queries behind these tables

Most of the numbers above come from two queries, and the first counts names against Unicode ranges.

SELECT
  count(*) AS contacts,
  count(*) FILTER (WHERE full_name ~ '[؀-ۿ]') AS arabic_script,
  count(*) FILTER (WHERE full_name ~ '[぀-ヿ一-鿿]') AS japanese_script,
  count(*) FILTER (WHERE full_name ~ '^[A-Za-zÀ-ɏ .''-]+$') AS latin_only,
  count(*) FILTER (WHERE full_name ~ '[؀-ۿ]' AND full_name ~ '[A-Za-z]') AS mixed_arabic_latin,
  count(DISTINCT full_name) AS distinct_names
FROM crm.contacts;

The second counts formats with regexes. Four of its ten columns are shown.

SELECT
  (SELECT count(*) FROM crm.addresses WHERE country_code = 'DE' AND postal_code ~ '^\d{5}$') AS de_postal_5_digits,
  (SELECT count(*) FROM crm.addresses WHERE country_code = 'JP' AND postal_code ~ '^\d{3}-\d{4}$') AS jp_postal_nnn_nnnn,
  (SELECT count(*) FROM crm.contacts WHERE phone ~ '^\+49') AS phone_plus49,
  (SELECT count(*) FROM crm.contacts WHERE phone ~ '^\+81') AS phone_plus81;

Formats with nothing to do with locale check the same way. On this schema a brief asking for invoice numbers from INV-2026-0001 gave 960 of 960 rows matching number ~ '^INV-2026-\d{4}$', up to INV-2026-0960.

How to ask for localized test data

Name the tables you care about and put a number on each one, since a table the sentence mentions without a count is left for the run to size. A share written as a percentage of a stated total gives the run something exact to hit, the way the three-country scope named 50, 30 and 20 against 200 customers. Give the locale for people, phone numbers and street addresses a clause separate from the one saying where the company is based, and say which script the values belong in. Before a demo, read a sample of the free-text columns with the queries above, the check that turned 300 rows of Arabic script into a number you can assert on.

Start from the DDL on scope examples if you want to reproduce any run on this page; every table above came from that file.