Laravel in Ottawa
Laravel development for Ottawa government and association systems
A Laravel development company for Ottawa departments, agencies and associations. Content that cannot publish until it exists in both official languages, which is a workflow in the schema.
An Ottawa system has a bilingual obligation, and the mistake is treating it as a presentation concern.
Interface labels in two languages are the easy part and the part every framework handles. The demanding part is content - a notice, a programme description, a form, a news item - where both language versions are the record, both have to exist before anything is published, and the second one arrives days after the first from somebody outside the application.
That makes it a workflow with states, and workflows with states belong in the schema.
One Ottawa record, a row per language
The shortcut is a column per language - title_en and title_fr - and it works until there are eight translatable fields, at which point "is this record complete" becomes a question you answer by checking sixteen columns.
A translations table instead holds one row per language per record. Completeness becomes a count. The publication condition becomes a single expression. Adding a third language later - which happens, for Indigenous languages or for an audience a programme serves - becomes a row rather than a migration across every table.
The publication state belongs to the record rather than to a language version, because the thing being published is the content and not one of its translations.
The publish gate has to be in the Laravel model
A rule that content cannot go live until both languages exist is only a rule if nothing can bypass it.
Enforced in a controller, it is enforced for the one path somebody thought about. An import, a console command, a scheduled job or next year's new admin screen writes the row directly and nothing objects. For an obligation an organisation states publicly, that is not a sufficient guarantee.
So the condition lives in the model, applied on every save, with an override that is explicit and recorded rather than a gap in the checking. When an exception is genuinely needed, somebody has made a decision that is visible afterwards. That is the same reasoning as authorisation belonging at the data rather than in the controller, applied to publication instead of access.
Translation as a tracked state, not a nullable column
Translation happens outside the application. A record is drafted, sent, returned, reviewed and approved, and each of those is a transition with a date and a person attached.
Modelled as states, the system answers the questions people actually have. What is waiting to go out for translation. What has been with the translator for three weeks. What came back and nobody has reviewed. How long the round trip takes on average, which is the number that lets somebody plan a launch.
Modelled as a nullable column, it answers none of those, and the coordination moves into a spreadsheet beside the application - which is where bilingual content schedules go to be forgotten.
Edits make Ottawa translations stale, and the system should say so
The last piece is the one that causes quiet drift.
When the English text is edited after the French was approved, the French is now a translation of something that no longer exists. Nothing in a naive model notices. The site serves both, they disagree, and the discrepancy is found by a reader rather than by the organisation.
A version on the source and a recorded source version on the translation makes the mismatch detectable, reportable and - importantly - something that can block publication of the edit until the translation catches up. The country-level ground for Canada is on the Canada market page.
An Ottawa search box has to find French content from an English screen
Bilingual search is not a translated placeholder. It is two indexes, two sets of linguistic rules, and a decision about what a user searching in one language should be shown of the other.
Stemming differs by language and using the wrong one quietly degrades results - a French dictionary applied to English text, or more commonly a default English configuration applied to everything, so that French plurals and verb forms do not match. In Postgres that means a text search configuration per language column and an index for each, not one index over a concatenation of both. Accent folding is a separate choice: a user typing "procedure" should find "procédure", which requires an unaccented variant to search against, kept alongside rather than instead of the accented text.
Then the policy question. A user working in English searches for a term and the only matching document is French. Hiding it is defensible and often wrong, because for government content the French document may be the only one that exists on that subject. Showing it, labelled, with its language visible, is usually the better answer - and it is a product decision that has to be made explicitly rather than falling out of how the query was written.
Ranking across two languages needs care as well, since scores from two different configurations are not directly comparable. Searching each index and merging with a deliberate rule beats a single query that looks simpler and ranks arbitrarily.
Laravel correspondence in an Ottawa recipient's own language
Language of correspondence is an attribute of the person, recorded once and honoured everywhere. That sounds obvious and it is routinely broken by the quiet fallback.
The fallback is the problem. A template exists in English, the French version was never written, and the notification goes out in English to somebody who elected French. Nothing failed, no error was raised, and the obligation was breached. The fix is to make a missing translation an error in the pipeline rather than a silent substitution - the notification is held and flagged, which is uncomfortable exactly once and then gets the template written.
Documents are harder than emails because they are composed rather than templated. A generated PDF pulls in a record's own stored text, and that text has its own language, which may not be the recipient's. A letter in French quoting an English case note is the realistic output, and deciding in advance which parts are translated, which are quoted verbatim and how the document says so is part of the design rather than something to discover at review.
The operational detail that saves the most time is a per-locale preview that the client's own bilingual staff can open without a developer. Translation review is their work, not ours, and the only thing that makes it practical is being able to see the rendered output in both languages side by side before anything is sent.
Commercially
Scope fixed in writing before work starts, priced per project.
Invoiced by Codefacture Yazılım A.Ş. in Türkiye, in Canadian or US dollars by bank transfer, with no Turkish VAT under the export-of-services exemption.
