The Vernacular Data Dilemma
When building enterprise SaaS for urban professionals, you can confidently hardcode your application in English. However, when architecting an Agritech platform for Indian farmers, English-only interfaces result in zero adoption. The platform must natively support regional languages—specifically Gujarati and Hindi—to deliver genuine value.
At Smart Tech Devs, we engineered KhedutBandhu (ખેડૂત બંધુ), a comprehensive agricultural ecosystem that serves real-time APMC Mandi commodity pricing and agronomic advisories. A major architectural challenge we faced was dynamic data translation. It is easy to translate static UI buttons using standard Laravel localization files (lang/gu.json). But how do you translate dynamic database records? If the admin panel inserts a new crop like "Wheat," the database must simultaneously serve "Wheat" to English users, "गेहूं" to Hindi users, and "ઘઉં" to Gujarati users.
Creating separate database rows for each language destroys data integrity. Creating separate columns (name_en, name_gu, name_hi) requires complex, brittle schema migrations every time a new language is added. To solve this, we architected a flexible, high-performance translation layer using PostgreSQL JSONB columns and custom Eloquent Traits in Laravel 11.
Phase 1: Architecting the JSONB Schema
Modern relational databases like PostgreSQL (and newer versions of MySQL) offer native JSON support. By storing dynamic translations as a single JSON object inside a JSONB column, we achieve a schema-less translation architecture. We can add Marathi or Punjabi tomorrow without running a single database migration.
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('crops', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
// 1. The JSONB column stores all translations in one field
// Example payload: {"en": "Wheat", "gu": "ઘઉં", "hi": "गेहूं"}
$table->jsonb('name_translations');
$table->string('category'); // e.g., Grains, Pulses
$table->timestamps();
// 2. We can even index specific JSON keys in Postgres for fast searching
$table->rawIndex("(name_translations->>'gu')", 'crops_name_gu_index');
});
}
};
Phase 2: The Translatable Eloquent Trait
We do not want our controllers dealing with raw JSON parsing. We architected a custom Eloquent Trait that hooks into Laravel's Mutators and Accessors. When the KhedutBandhu API requests a crop name, this Trait automatically reads the Accept-Language HTTP header and returns the correct regional string.
namespace App\Traits;
use Illuminate\Support\Facades\App;
trait HasTranslations
{
/**
* Decode the JSON column automatically when reading from the database
*/
public function getAttributeValue($key)
{
$value = parent::getAttributeValue($key);
if (in_array($key, $this->translatable ?? [])) {
$translations = json_decode($value, true) ?: [];
// 1. Determine the active locale (set by Middleware via headers)
$locale = App::getLocale();
// 2. Return the requested language, fallback to English, or return the raw string
return $translations[$locale] ?? $translations['en'] ?? $value;
}
return $value;
}
/**
* Encode translations safely back into JSON before saving
*/
public function setAttribute($key, $value)
{
if (in_array($key, $this->translatable ?? []) && is_array($value)) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
}
return parent::setAttribute($key, $value);
}
}
Phase 3: API Delivery and Timezone Synchronization
With our Trait active, our Eloquent Models become incredibly powerful. In our Crop model, we simply declare public $translatable = ['name_translations'];.
When the Flutter mobile app requests the daily APMC Mandi prices, it sends a header: Accept-Language: gu. Our global localization middleware sets the Laravel App Locale to gu. Our API controller then fetches the data exactly as normal, but the JSON response is perfectly localized for the farmer.
namespace App\Http\Controllers\Api;
use App\Models\MandiPrice;
use Illuminate\Http\Request;
class MandiPriceController
{
public function index(Request $request)
{
// 1. Fetch the latest prices. The 'HasTranslations' trait will
// automatically convert the crop names to Gujarati or Hindi.
$prices = MandiPrice::with(['crop', 'mandi'])
->whereDate('created_at', now('Asia/Kolkata')->toDateString())
->get();
return response()->json([
'status' => 'success',
// Return timestamp explicitly in IST so farmers know exactly when the market closed
'last_updated' => now('Asia/Kolkata')->format('d-m-Y h:i A'),
'data' => $prices->map(function ($price) {
return [
'mandi_name' => $price->mandi->name_translations,
'crop_name' => $price->crop->name_translations,
'min_price' => $price->min_price,
'max_price' => $price->max_price,
'modal_price' => $price->modal_price,
'trend' => $price->getTrendIndicator(), // Returns ↑ or ↓
];
})
]);
}
}
The Engineering ROI
By architecting our vernacular data utilizing PostgreSQL JSONB columns and dynamic Eloquent accessors, KhedutBandhu achieves ultimate scalability. Our administration team can instantly add hundreds of new crops and APMC Mandis via the backend /admin panel, injecting English, Gujarati, and Hindi strings into a single database row. The mobile app and public web portal automatically serve the correct dialect without a single if/else statement cluttering our controllers. This unified, schema-less approach eliminates technical debt and guarantees that our critical market intelligence reaches rural farmers in the exact language they understand.
Top comments (0)