The Turkish dotted İ, CSS and uppercase English terms
On a Turkish page, text-transform: uppercase turns i into İ. Right for Turkish words, wrong for JavaScript or Firebase. On this site I solved it with the bilingual data itself rather than a hand-kept word list, and the same rule runs on the share cards too.
01The browser is actually right
On this site, small mono text, tags, filter buttons and menu links are uppercased by CSS. In base.css, the .mono, .tag, .chip and .lk classes all have text-transform: uppercase. The source text stays in lowercase; the browser does the uppercasing while it draws the text.
To do that, the browser looks at the language, because case mapping is not universal. In English, i becomes I. In Turkish, four letters form two separate pairs: dotless ı with I, and dotted i with İ. When the page is lang="tr", the browser applies the Turkish mapping and the label "Mühendislik" becomes MÜHENDİSLİK. That is exactly what we want. Under lang="en" the same word would become MÜHENDISLIK, which is wrong for a Turkish reader.
The problem is that the same rule also applies to technology and project names. JavaScript becomes JAVASCRİPT, Firebase becomes FİREBASE, Linux becomes LİNUX. The browser is not making a mistake here; we are the ones telling it the text is Turkish. On this site the <html lang> value is bound in App.vue through useHead to the language taken from the URL. So on Turkish pages, unless told otherwise, everything uppercases by the Turkish rule.
02First attempt: a word list
My first reflex was to collect the words that should stay English in a list and give matching labels lang="en". It worked with the demo content. Then real content went into the projects and the About page: Go, Dart, gRPC, Kubernetes and NVIDIA Jetson Nano joined the tool list, and project names and interests changed. The list went stale overnight and the labels showed the dotted İ again.
The real flaw of the list was not that it went stale, but that it did not tell anyone. When I added a new tool nothing broke, the build passed, and a single label just quietly looked wrong. Bugs like that only get noticed when someone looks at the page carefully.
A hand-kept list breaks quietly as the content grows.
03The rule: identical in both languages means a name
The site is already bilingual. Every label has a Turkish and an English version under src/i18n/locales, and that data already holds the information the list was trying to keep. If a label is identical in both languages, it was not translated, and something that is not translated is almost always a name or a term: Linux, Firebase, Motice, Blockchain. If the two differ (Sensör haberleşmesi / Sensor communication), it is a translation and should stay in the page language.
export const sameInBoth = (a: string | undefined, b: string | undefined) =>
a !== undefined && a === b ? 'en' : undefined
/** Project stacks: always technology names. */
export const STACK_LANG = 'en'
export const catLang = (c: ProjectCat | 'all') =>
sameInBoth(tr.cats[c], en.cats[c])
export const toolTagLang = (group: number, item: number) =>
sameInBoth(tr.about.tools[group]?.items[item], en.about.tools[group]?.items[item])The function returns either "en" or undefined. undefined is a deliberate choice: Vue does not write an attribute whose value is undefined, so the label inherits the page language. All a component does is write :lang="catLang(c)". Four helpers go through the same rule: catLang for project categories, fieldTagLang for the field tags on the home page, toolTagLang for the tools on the About page and outsideLang for interests.
Technology stacks are a separate case. A project's stack comes from a single, language-neutral array, so there is no second language to compare against. Every item there is already a technology name, so ProjectView uses STACK_LANG, which is "en", directly.
04CatLabel and the edge cases
A project can have more than one category, shown on a single line such as "Web / Platform · Mobil". Giving the whole line one lang would be wrong: Blockchain should uppercase the English way, Mobil the Turkish way. That is why CatLabel.vue puts each category in its own span and gives each span its own lang value; the separators stay in the page language.
It also helps to know where the rule stops. The comparison looks for exact equality and is case-sensitive. "Web / Platform" and "Web / Platforms" differ, so the label uppercases by the Turkish rule; since it has no i, the result does not change. "Edge sistemler" stays Turkish and becomes EDGE SİSTEMLER, which is right because half of it is Turkish. Field and tool tags are matched by their position in the array, so if the order of the Turkish and English lists drifts, the rule compares the wrong two tags. If a Turkish word happens to be spelled the same as its English counterpart, the rule takes it for a name; I have not seen such a case among the current labels. I accepted these knowingly, because the two language files already share the same structure.
05The same rule on the share cards
Every page's 1200×630 share card is drawn with Satori at build time, and the section label at the top of the card is uppercase there too. But Satori has no page where the lang attribute and text-transform work together; JavaScript does the uppercasing. The difference between toLocaleUpperCase('tr') and toLocaleUpperCase('en') is the exact counterpart of the lang difference in CSS. scripts/seo-files.mjs repeats the same comparison for category labels.
const up = (s) => s.toLocaleUpperCase(lang === 'en' ? 'en' : 'tr')
// same rule as src/utils/lang.ts
const upCat = (c) => (tr.cats[c] === en.cats[c]
? d.cats[c].toLocaleUpperCase('en')
: up(d.cats[c]))So a category that shows as BLOCKCHAIN on the site does not turn into BLOCKCHAİN on the card. The rule is written in two places, but both look at the same data; when I add a new category I do not have to update either of them separately.
06Reusing it in another project
You do not need bilingual content to carry this approach over, but it makes the job much easier. The general idea has three parts: do not assume case mapping is language-neutral, think of the lang attribute at the level of a piece of text rather than the page, and derive the "is this English" decision from data rather than by hand. Even on a single-language site, keeping terms in a separate field and giving them lang="en" holds up better than a word list. Leaving the uppercasing to CSS has a side benefit too: the source text stays in its natural form, and the same data can be used as is wherever no uppercase is needed.