Skip to main content
Back to all posts
Oct. 20268 min read

A $1.35 fine-tune taught an open 4B model that CRA is ARC in French. It still writes NIS for SIN.

An open 4B model translated "a business number from the CRA" and wrote RRCQ, an agency that does not exist. I fine-tuned it on Tinker for about $1.35 on Government of Canada translations. On held-out canada.ca pages it now writes the government's own French acronym 66.9% of the time, against 50.8%. Every flag it raises cites its source, and it runs on my laptop.

HacktoberfestDEV ChallengeTinkerQwen3.5Fine-tuningCanadian FrenchOpen dataLocal inferenceHackathon
I wrote this post for the DEV Hacktoberfest Open-Source AI Challenge, Week 2: Side Hustle (October 2026), and built Sans calque for the same submission. Live at sans-calque.51-161-82-166.sslip.io while the Tinker credit lasts (it has a daily word cap), source at github.com/JonathanSolvesProblems/sans-calque. #hf26challenge
The claim in one sentence: on held-out canada.ca small-business pages, whose French was written by Government of Canada translators, the fine-tune writes the government's own French acronym 66.9% of the time against 50.8% for the untouched model, ahead in all 1,000 bootstrap resamples. Not claimed: better translation overall. On chrF++ it is ahead in 92% of resamples, short of the 95% bar I set before looking.
The Sans calque light table: the English sentence "Before you register your business, you need a business number (BN) from the CRA", the untouched Qwen3.5-4B's French with RRCQ circled, the fine-tuned French reading de l'ARC with a check mark, and 66.9% of government acronyms written correctly.
Recorded output from the deployed models, not a mock-up. The untouched model invents an agency; the fine-tune writes what the government writes.

There is no RRCQ

Every page on my site has a French twin. I build things on the side (send me a brief, get a working demo back in 48 hours), and the site ships in English and fr-CA, so every new page means a translation. So I gave an open 4B model a line any Canadian small-business page would have: "you need a business number (BN) from the CRA". It wrote "auprès du RRCQ". There is no RRCQ. The Canada Revenue Agency is the ARC in French, and in Canada's open data the government's translators wrote ARC all 31 times the pair shows up.

An Open Government Portal record quoted word for word: the English text "The CSFA Program and CRA use a risk-based sampling methodology" beside the French by Government of Canada translators, "Le PCAFE et l'Agence du revenu du Canada (ARC) utilisent", with CRA and ARC highlighted, and a note that CRA became ARC in 31 of 31 pairs.
One of the 31 pairs. The evidence for every acronym flag is a record like this in the government's own open data.

Who it is for

A small business pays a bilingual freelancer per page to put its site into French. The client won't read the French closely, but they will notice CRA where it should say ARC. With an open model on the freelancer's own laptop there is no per-word bill, and the client's text never leaves the machine. Sans calque gives that freelancer French from the fine-tune next to the untouched base model, plus a review list that flags three things: an English acronym left in the French, a missing French acronym the government uses (ARC for CRA, NAS for SIN, from 466 pairs learned from open data), and a term the Translation Bureau's Termium Plus says to avoid.

Termium Plus open data for "fiscal year" under Public Administration (General): exercice financier and année financière marked correct and standardized, exercice marked correct, and année fiscale highlighted as avoid, calque.
Termium Plus labels année fiscale a calque to avoid. Every term flag cites an entry like this one.

Nothing is fixed silently. On "most small businesses use the calendar year as their fiscal year", the untouched model writes année fiscale and the fine-tune writes année financière. Both models write "comptes à payer" for accounts payable, which Termium also flags, and the check catches it on both. A flagged sentence lands on the review list, and the "French, ready to send" box says how many blocks to check before it goes to the client.

The results view: each English sentence followed by the untouched model's French and the fine-tuned French, with RRCQ, année fiscale and comptes à payer highlighted, and margin notes on the right giving each rule and its source, such as 31 of 31 government pairs or Termium Plus.
The sample on the live site. Anything that shows through is circled, with where the rule came from.

Trained on the government's own open text

The data is all open. Every dataset on Canada's Open Government Portal has its title and description in both languages, under the Open Government Licence. I pulled all 48,116 records, dropped the ones whose French the portal marks as machine-translated, and paired paragraphs and sentences only when both sides split the same way. That gave 82,455 pairs. The fine-tune is two runs on Tinker: Qwen3.5-4B, LoRA rank 32, 8,000 pairs, 2 epochs, about 13 minutes each. Run 1 was a random sample. Run 2 drew 2,000 of its pairs from the 10,752 that contain a known acronym, because run 1 still wrote "de la RC" for CRA. Estimated cost, from Tinker's per-token price and the real token count: $1.26 and $1.35. The training script refuses to start if the estimate goes over budget, because the credit was a one-time $10.

Graded by the government's translators

I scored everything on canada.ca small-business pages, whose French was written by Government of Canada translators: 300 random sentences, plus all 147 that touch a Termium "avoid" concept. None of those sentences were in training. The government's French acronym came out right 36.0% of the time from Gemma 3 4B, 50.8% from the untouched Qwen3.5-4B, 55.8% from run 1 and 66.9% from run 2. English acronyms left in the French went from 41 to 10. The acronym gain holds up: run 2 beats the base model in all 1,000 bootstrap resamples of the 146 held-out sentences that contain a government French acronym. Overall quality does not: on chrF++ (55.32 against 56.09) run 2 is ahead in 92% of resamples, and I had set 95% as the bar before running it.

The How it was graded table: Gemma 3 4B, the untouched model and the fine-tune compared on government French acronyms written correctly (36.0%, 50.8%, 66.9%), English acronyms left in the French (47, 41, 10), sentences with a Termium avoid term (16, 24, 14), chrF++ (50.58, 55.32, 56.09) and COMET (0.759, 0.789, 0.785).
The gain is acronyms and terms, not overall quality, and the page says so under the table.

It still makes mistakes

It is not always right, and the base model sometimes is. On "you can apply for your SIN", the base model writes "numéro d'assurance sociale (NAS)", which is correct. Run 2 writes "NIS". The acronym check catches it, because the government wrote NAS in 10 of 10 pairs. Run 2 also wrote "Conserver" where an instruction needs "Conservez", and none of my checks catch that one. Run 2 isn't better everywhere either: it leaves more English acronyms and more Termium terms than run 1. I serve it anyway because it gets the acronyms a client notices, and the box still says to read the French before you send it.

The SIN sentence: the untouched model writes numéro d'assurance sociale (NAS), the fine-tune writes NIS, highlighted, with the margin note "NIS should be NAS for SIN, 10 of 10 government pairs", above the French, ready to send box reporting 2 of 5 blocks to check.
The fine-tune gets this one wrong and the base model gets it right. The check catches it before it reaches the client.

The checks got checked too

My first Termium check flagged the government's own translators 21 times out of 25. The label it was counting, "avoid, see observation", means avoid in some contexts only, so I dropped it. What is left flags the human translation once, for "année fiscale", which Termium calls a calque and canada.ca still uses. I left that one in. A script, check_claims.py, rebuilds every number in the README from the results files and fails if the prose and the data disagree. It also fails if a model output quoted in the docs is not in the recorded outputs.

The weights are mine

I exported the fine-tune from Tinker, converted it to a 2.59 GB 4-bit file, and it runs in Ollama on my laptop's 8 GB GPU at 32.5 tokens a second. The same web page runs against it: I loaded it with every request outside my laptop blocked, and it still translated, fonts included, with zero requests out. That took three dead ends: Ollama no longer accepts LoRA adapters, its own quantiser needs a Mac, and an 8-bit file ran at 0.8 tokens a second. Shrinking to 4-bit can cost something, so I scored the laptop copies on the same test. For run 1 it did (chrF++ 54.96 against 55.65 in the cloud). For run 2 it did not: 56.43 against 56.09, and 67.8% of the government's acronyms against 66.9%. Statistically those are the same model, so I read it as nothing lost, not as the laptop being better.

Local mode: the header reads On this computer, through Ollama. The untouched local model writes RReC for CRA and année fiscale, both flagged, while the fine-tune writes de l'ARC and année financière.
The same page on my laptop with every outside request blocked. The local untouched model invents a different acronym, RReC, and it still gets caught.

My first user was my own site

I pasted the English "How I work" page from this site into the live app: 41 blocks. The fine-tune raised 8 flags. 7 were noise for this kind of text: MCP, LLM, RAG, FHIR, SDK and a NICE guideline code, all acronyms my own French page keeps in English. The acronym rule learned how the government writes, and tech jargon does not follow it. The 8th was real. My sentence says the logs held data "the AI had already claimed was PHI-free", and the French came back as "lesquels avaient déjà été déclarés exempts", which means "which had already been declared free". The AI is gone from the sentence, and it was the whole point of it. So I built what that told me to build: a "keep in English" list per client, and a "Fetch the page" box so a page comes in by its address. Same page, fetched by the app, with those terms on the list: 8 flags became 1, and the one left is the real catch.

The French, ready to send box for my own How I work page, fetched by address: 1 of 41 blocks flagged, followed by the French translation of the page.
My own page with MCP and SDK kept in English: 1 of 41 blocks flagged, down from 8, and that one is real.

What is not claimed

The headline number is graded by text I did not write: Government of Canada translators on held-out canada.ca pages, which is the only reason it is worth anything. It is still narrow. The gain is in acronyms and terms; overall translation quality is not measurably better, and I set the bar for saying so before I looked. The checks only know what the government's open data and Termium know, so they miss errors like "Conserver" for "Conservez", and they over-flag tech acronyms until a client's keep-in-English list is set. The model learned from government catalogue text, so it has no particular flair for marketing copy. And the recipe (fine-tune on a government's open bilingual text, then check every term against its term bank) should carry to places like the EU's IATE, but I have only measured it here.

Related project

Sans calque: English to Canadian French from an open 4B model I fine-tuned for $1.35 on government translations, with every flag citing where its rule came from

View the project