Why 'Choose a unique column name.' Depends on Your Locale: The Data Studio's Column-Name Rule

2026-11-06来源 dev.toEnglish

I work on OnePassApps, so this is a measurement of our own build, read out of the code we ship.

A reader of my post on duplicate CSV headers asked me a fair question and then kept asking it, in a very long thread, and I answered it 48 times. So this post is the answer, written once, where it can be found.

The question was: when the Data Studio refuses a column name with "Choose a unique column name.", what exactly is it comparing?

The answer, read out of the bundle

I fetched the Data Studio's route over plain HTTPS and walked the JavaScript it loads. The Data Studio's data operations do not run in the page. They run in a Web Worker:

/assets/data-operation.worker-nH-m3g4I.js — 54,757 bytes on the wire (54,755 characters; the file carries a byte-order mark).

That worker file is the only file in the Data Studio's chunk graph that contains the string Choose a unique column name. I crawled 203 chunk files reachable from /studios/data and looked for it in every one.

Inside that worker, the rename check is one expression:

e.headers.some((e,n)=>n!==t&&e.toLocaleLowerCase()===r.toLocaleLowerCase()) 

Three things about it are worth stopping on.

It is toLocaleLowerCase(), with no argument. Not toLowerCase(). Not a collation. The no-argument form reads the runtime's default locale, so the comparison follows the host's locale settings. In the whole 54,757-byte worker file there are 4 calls to toLocaleLowerCase(), 0 calls to toLowerCase(), and 0 calls to .normalize(.

It compares whole strings. The equality is on the entire lowercased name, not on a prefix or a word.

The column being renamed is excluded from its own comparison. That is the n!==t term. It is why renaming a column to a case variant of its own name succeeds.

And before any of that, the candidate name is trimmed:

let r=n.trim(); 

So the rule is: trim the candidate, lowercase both sides with the runtime's locale, compare for whole-string equality, and ignore the column being renamed.

What that rule means in practice

Because the lowercasing is locale-sensitive, the verdict is not the same everywhere. İ (U+0130, Latin capital letter I with dot above) lowercases to i plus a combining dot above in a US-locale runtime, but to plain i in a Turkish-locale one. So the pair İ and i is two different names in one runtime and the same name in another.

The rename path is not the only place the locale shows up. The same worker holds two more comparisons:

// the filter: a SUBSTRING test, not equality let r=t.toLocaleLowerCase(); ... .some(e=>e.toLocaleLowerCase().includes(r)) 
// the sort: a collation with no locale argument localeCompare(other,void 0,{numeric:!0,sensitivity:`base`}) 

The filter folds case and then tests for a substring, so a filter query can match part of a cell rather than the whole of it. The sort uses base sensitivity, which means İ, i and I can tie with each other — and a collation that ties two strings is deciding they are equal.

All three sites live in the same worker file. That has a consequence worth stating plainly: the locale enters at the comparison, not at the data. Patch String.prototype.toLocaleLowerCase in the page's main world and the rename verdict will not move, because the deciding code is not running there.

The parse side is a different rule entirely

The duplicate-header check happens elsewhere, in the CSV parsing step, and it is not a fold at all. It is a byte comparison:

var i=w[0],a=Object.create(null),o=new Set(i); ... if(a[r=x(e.transformHeader)?e.transformHeader(r,n):r]){ ... } 

transformHeader is never set by our app — it appears in the bundle only as the library's own parameter — so the header string is used exactly as written. Two headers collide only when they are byte-identical. İ,ı loads as two clean columns with no parsing issue; only İ,İ collides, and then the second one is minted as İ_2.

When a collision does happen, the mint takes the first free _N slot in the family:

for(;e=r+`_`+s,s++,o.has(e);) 

The suffix is the first free slot, not the next one after the highest number in play.

So there are two rules, and they disagree

Surface Comparison Follows the locale?
Duplicate headers (parse) byte-identical strings no
Manual rename (Columns tab) whole-string toLocaleLowerCase() equality yes
Filter (Sort / filter tab) toLocaleLowerCase() substring yes
Sort (Sort / filter tab) localeCompare, base sensitivity, no locale argument yes

The parse side is case-sensitive and locale-blind. The rename side is case-insensitive and locale-sensitive. Over one namespace of names, in one build.

That means a file with headers extra 1 and Extra 1 loads cleanly as two distinct columns, and then the rename path refuses extra 1 as a duplicate of Extra 1 — the collision exists at the surface where you can act on it and never at the surface that created it.

Where I would change it

One comparison rule for one namespace.

The rename path already has the strict behaviour the other surfaces lack. If the parse side folded the same way, extra 1 and Extra 1 would either be recognised as one name at load time — and reported — or the added column would step to a name that cannot collide with the reader's.

And the locale dependence is the part I would want gone regardless of the rest. A uniqueness check whose answer changes with the user's locale settings is a check whose answer depends on the machine rather than on the data. Pinning the comparison to an explicit locale — toLocaleLowerCase("en-US"), or a plain toLowerCase() — makes it the same question everywhere. The same argument applies to the sort's collation: base sensitivity ties İ, i and I, and those ties move with the host locale. A tie in a sort is an equality decision wearing an ordering costume.

Limits, stated honestly

  • One build, one browser, read on 6 November 2026. The file name carries a content hash, so the next build may ship a different one.
  • The source is what the server served over plain HTTPS today, with no session. I read the code, not the running behaviour — the runtime verdicts quoted here come from separate measurements of the live Data Studio, and the worker is why a main-world patch could not move them.
  • I did not test a real Turkish-locale host. A CDP locale override moves the İ mapping but not the plain I mapping, so the dotless-i pair is measured only against an emulated runtime, not a genuine one.
  • The filter and sort findings above are read from the source. The rename verdict measurements behind the table are from the live tab.
  • One direction only: I did not sweep the whole Unicode range, and the two rules are described as they behave for the pairs I tested.

If you want to see the same behaviour yourself, the Data Studio is at https://app.onepassapps.com/studios/data?utm_source=devto&utm_medium=post&utm_campaign=data-studio-naming-rule&utm_content=data-studio-link, and the app guides are at https://app.onepassapps.com/guides?utm_source=devto&utm_medium=post&utm_campaign=data-studio-naming-rule&utm_content=guides-link

原文:https://www.a8q9.cn/en/article/why-choose-a-unique-column-name-depends-on-your-locale-the-data-studio-s-column--d2518b.html

相关文章