All examples
Multilingual price list · Pick language and format in the frame header
Switch the language directly in the preview header. Report texts come from the module lang directory; dates and numbers follow the format locale, while the currency stays CHF.
Flow
Language
Aggregates
/reports/multilanguage/run
What is typical here
- Language selection in the report frame header
- Module translations under app/Reports/Multilanguage/lang
- Product, category and status texts come from language arrays in the source (like Spatie Translatable)
- Dates and numbers follow the format locale (de_CH vs. en_US)
- The currency stays CHF independently of the language
- The switch applies to HTML preview and PDF
Module structure
app/Reports/Multilanguage/
├── Report.php # locale-aware report
├── Source.php # language-neutral demo rows
├── lang/
│ ├── de/report.php
│ └── en/report.php
└── views/
├── report-start.blade.php
├── grid-head.blade.php
├── row.blade.php
└── report-end.blade.php
Files
<?php
declare(strict_types=1);
namespace App\Reports\Multilanguage;
use Guggach\Reports\Aggregates\Count;
use Guggach\Reports\Aggregates\Sum;
use Guggach\Reports\Definition\ReportBuilder;
use Guggach\Reports\Report as BaseReport;
use Guggach\Reports\Sources\ReportSource;
/**
* Flow report with module translations and locale-aware dates/numbers.
* The language selector in the preview frame changes the run locale; this
* report then renders all labels, dates and amounts in that language.
*/
final class Report extends BaseReport
{
public function key(): string
{
return 'multilanguage';
}
public function name(): string
{
return __('multilanguage::report.name');
}
public function layout(): string
{
return 'default';
}
public function currency(): string
{
return 'CHF';
}
public function source(): ReportSource
{
return new Source;
}
public function define(ReportBuilder $r): void
{
$r->reportStart(view: 'report-start');
$r->gridHeader(view: 'grid-head');
$r->detail(view: 'row');
$r->reportEnd(view: 'report-end');
$r->aggregate('total', Sum::class, field: 'price', scope: 'report');
$r->aggregate('positions', Count::class, field: 'product', scope: 'report');
}
}
<?php
declare(strict_types=1);
namespace App\Reports\Multilanguage;
use Guggach\Reports\Localization\ReportLocale;
use Guggach\Reports\Sources\ReportField;
use Guggach\Reports\Sources\ReportSource;
/**
* Language-neutral demo data. In a real application product, category and
* status would come from Eloquent models with spatie/laravel-translatable;
* the reports engine sets app()->getLocale() before the source is queried.
* This fixture demonstrates the same idea with language arrays.
*/
final class Source extends ReportSource
{
public function records(): iterable
{
$language = $this->language();
$records = [
[
'product' => ['de' => 'Studio Miete', 'en' => 'Studio rent'],
'category' => ['de' => 'Miete', 'en' => 'Rent'],
'price' => 2400.00,
'valid_from' => '2026-09-18',
'status' => ['de' => 'Aktiv', 'en' => 'Active'],
'status_key' => 'active',
],
[
'product' => ['de' => 'Proberaum Service', 'en' => 'Rehearsal room service'],
'category' => ['de' => 'Service', 'en' => 'Service'],
'price' => 860.00,
'valid_from' => '2026-09-22',
'status' => ['de' => 'Aktiv', 'en' => 'Active'],
'status_key' => 'active',
],
[
'product' => ['de' => 'Bühnenlicht Paket', 'en' => 'Stage lighting package'],
'category' => ['de' => 'Verkauf', 'en' => 'Sale'],
'price' => 12500.00,
'valid_from' => '2026-10-02',
'status' => ['de' => 'Aktiv', 'en' => 'Active'],
'status_key' => 'active',
],
[
'product' => ['de' => 'Künstler-Workshop', 'en' => 'Artist workshop'],
'category' => ['de' => 'Service', 'en' => 'Service'],
'price' => 1450.00,
'valid_from' => '2026-10-05',
'status' => ['de' => 'Entwurf', 'en' => 'Draft'],
'status_key' => 'draft',
],
[
'product' => ['de' => 'Tanzboden Miete', 'en' => 'Dance floor rent'],
'category' => ['de' => 'Miete', 'en' => 'Rent'],
'price' => 1800.00,
'valid_from' => '2026-10-08',
'status' => ['de' => 'Aktiv', 'en' => 'Active'],
'status_key' => 'active',
],
[
'product' => ['de' => 'Requisiten Verkauf', 'en' => 'Props sale'],
'category' => ['de' => 'Verkauf', 'en' => 'Sale'],
'price' => 7600.00,
'valid_from' => '2026-10-12',
'status' => ['de' => 'Entwurf', 'en' => 'Draft'],
'status_key' => 'draft',
],
];
return array_map(function (array $record) use ($language): array {
foreach (['product', 'category', 'status'] as $key) {
$record[$key] = $this->translated($record[$key], $language);
}
return $record;
}, $records);
}
/**
* @return list<ReportField>
*/
public function fields(): array
{
return [
new ReportField('product', __('multilanguage::report.field.product'), filterable: false),
new ReportField('category', __('multilanguage::report.field.category'), type: 'enum', filterable: false),
new ReportField('price', __('multilanguage::report.field.price'), type: 'number', filterable: false, aggregatable: true, format: 'currency'),
new ReportField('valid_from', __('multilanguage::report.field.valid_from'), type: 'date', filterable: false, format: 'date'),
new ReportField('status', __('multilanguage::report.field.status'), type: 'enum', filterable: false),
];
}
private function language(): string
{
$locale = $this->locale() ?? app()->getLocale();
return explode('_', ReportLocale::normalize($locale))[0];
}
/**
* @param array<string, string> $values
*/
private function translated(array $values, string $language): string
{
$value = $values[$language] ?? $values['en'] ?? reset($values);
return is_string($value) ? $value : '';
}
}
<?php
declare(strict_types=1);
return [
'name' => 'Mehrsprachige Preisliste',
'title' => 'Preisliste',
'intro' => 'Sprache und Formatierung folgen der Auswahl im Header. Die Währung bleibt unabhängig davon CHF.',
'language_label' => 'Sprache',
'format_label' => 'Format',
'columns' => [
'product' => 'Produkt',
'category' => 'Kategorie',
'valid_from' => 'Gültig ab',
'status' => 'Status',
'price' => 'Preis',
],
'summary' => [
'positions' => 'Positionen',
'total' => 'Gesamttotal',
],
'field' => [
'product' => 'Produkt',
'category' => 'Kategorie',
'price' => 'Preis',
'valid_from' => 'Gültig ab',
'status' => 'Status',
],
];
<?php
declare(strict_types=1);
return [
'name' => 'Multilingual price list',
'title' => 'Price list',
'intro' => 'Language and formatting follow the selection in the header. The currency stays CHF independently of the locale.',
'language_label' => 'Language',
'format_label' => 'Format',
'columns' => [
'product' => 'Product',
'category' => 'Category',
'valid_from' => 'Valid from',
'status' => 'Status',
'price' => 'Price',
],
'summary' => [
'positions' => 'Positions',
'total' => 'Total',
],
'field' => [
'product' => 'Product',
'category' => 'Category',
'price' => 'Price',
'valid_from' => 'Valid from',
'status' => 'Status',
],
];
<style>
.ml-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8mm;
border-bottom: 3px solid #1d4ed8;
padding-bottom: 4mm;
margin-bottom: 5mm;
}
.ml-head h1 { margin: 0 0 1mm; font-size: 22px; color: #0f172a; }
.ml-head p { margin: 0; font-size: 10px; color: #64748b; max-width: 120mm; }
.ml-locale {
display: flex;
gap: 4mm;
padding: 2mm 3mm;
border-radius: 2mm;
background: #dbeafe;
color: #1e40af;
font-size: 9px;
white-space: nowrap;
}
.ml-locale strong { display: block; font-size: 11px; }
.ml-grid-head,
.ml-row {
display: flex;
align-items: center;
gap: 3mm;
box-sizing: border-box;
}
.ml-grid-head {
height: 9mm;
border-bottom: 2px solid #1d4ed8;
font-size: 8px;
font-weight: 700;
color: #0f172a;
}
.ml-row {
min-height: 9mm;
border-bottom: 1px solid #e2e8f0;
font-size: 9px;
color: #334155;
}
.ml-col-product { flex: 1 1 auto; font-weight: 600; color: #0f172a; }
.ml-col-category { flex: 0 0 24mm; }
.ml-col-valid { flex: 0 0 28mm; color: #64748b; }
.ml-col-status { flex: 0 0 22mm; }
.ml-col-price { flex: 0 0 28mm; text-align: right; font-variant-numeric: tabular-nums; }
.ml-status {
display: inline-block;
padding: 0.8mm 1.6mm;
border-radius: 1mm;
font-size: 8px;
font-weight: 600;
}
.ml-status--active { background: #dcfce7; color: #166534; }
.ml-status--draft { background: #fef3c7; color: #92400e; }
.ml-summary {
display: flex;
justify-content: flex-end;
gap: 10mm;
margin-top: 4mm;
padding-top: 3mm;
border-top: 2px solid #0f172a;
font-size: 11px;
font-weight: 700;
}
</style>
<div class="ml-head">
<div>
<h1>{{ __('multilanguage::report.title') }}</h1>
<p>{{ __('multilanguage::report.intro') }}</p>
</div>
<div class="ml-locale">
<span>
{{ __('multilanguage::report.language_label') }}
<strong>{{ $ctx->locale }}</strong>
</span>
<span>
{{ __('multilanguage::report.format_label') }}
<strong>{{ $ctx->formatLocale }}</strong>
</span>
</div>
</div>
<div class="ml-grid-head">
<span class="ml-col-product">{{ __('multilanguage::report.columns.product') }}</span>
<span class="ml-col-category">{{ __('multilanguage::report.columns.category') }}</span>
<span class="ml-col-valid">{{ __('multilanguage::report.columns.valid_from') }}</span>
<span class="ml-col-status">{{ __('multilanguage::report.columns.status') }}</span>
<span class="ml-col-price">{{ __('multilanguage::report.columns.price') }}</span>
</div>
<div class="ml-row">
<span class="ml-col-product">{{ $ctx->product }}</span>
<span class="ml-col-category">{{ $ctx->category }}</span>
<span class="ml-col-valid">{{ $ctx->date($ctx->valid_from) }}</span>
<span class="ml-col-status">
<span class="ml-status ml-status--{{ $ctx->status_key }}">
{{ $ctx->status }}
</span>
</span>
<span class="ml-col-price">{{ $ctx->currency($ctx->price) }}</span>
</div>
<div class="ml-summary">
<span>{{ $ctx->aggregates['positions'] }} {{ __('multilanguage::report.summary.positions') }}</span>
<span>{{ __('multilanguage::report.summary.total') }} {{ $ctx->currency($ctx->aggregates['total']) }}</span>
</div>