TechSpoken
"Any ideas?" is the most frequently-asked question in technical forums. My answer is: yes.

Sensitivity versus Consistency in Global Communication

December 20, 2008 17:43 by LSN

 My colleague Wisdom posts a comment on this blog, and also asks

 Side talk, why i can't find China in the country List, when i am adding a comment?

Hmmm. 

In looking into this, I checked into the relevant BlogEngine.NET code, which looks something like the following (see CommentView.ascx.cs):

foreach (CultureInfo ci in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
{
   RegionInfo ri = new RegionInfo(ci.Name);
   if
(!dic.ContainsKey(ri.EnglishName))
   {
      dic.Add(ri.EnglishName, ri.TwoLetterISORegionName.ToLowerInvariant());
   }
   if (!col.Contains(ri.EnglishName))
   {
      col.Add(ri.EnglishName);
   }
}

... that looked perfectly fine. It does seem from other .NET framework communications that it is possible for this code not to be all-inclusive.  And, in fact, the BlogEngine.NET code underscores this potential issue, with the following code in the same method:

// Add custom cultures

if (!dic.ContainsValue("bd"))
{
   dic.Add("Bangladesh", "bd");
   
col.Add("Bangladesh" );
}

It also seems, on further investigation, that other blogs besides mine that are based on Blog.Net have seen this issue specifically with Chinese viewers. 

What's the real problem?

Cultural-appropriateness is in the eye of the beholder

In his response to the question specifically about BlogEngine, Mads Kristensen points out that China is on the list.  It's just listed according to its SpecificCulture-EnglishName value, which is People's Republic of China, not China

 

You can see this illustrated in the MSDN post on the CultureInfo class, in fact, where different "manifestations" of Chinese culture info are shown, in response to the following sample code:

foreach ( CultureInfo ci in CultureInfo.GetCultures( CultureTypes.SpecificCultures ) ) 
{
  if ( ci.TwoLetterISOLanguageName == "zh" )  {
      Console.Write( "0x{0} {1} {2,-37}", ci.LCID.ToString("X4"),
                            ci.Name, ci.EnglishName );
      Console.WriteLine( "0x{0} {1} {2}", ci.Parent.LCID.ToString("X4"),
                            ci.Parent.Name, ci.Parent.EnglishName );
   }
}

 

... like this:

SPECIFIC CULTURE

PARENT CULTURE

0x0404 zh-TW Chinese (Taiwan) 0x7C04 zh-CHT Chinese (Traditional)
0x0804 zh-CN Chinese (People's Republic of China) 0x0004 zh-CHS Chinese (Simplified)
0x0C04 zh-HK Chinese (Hong Kong S.A.R.) 0x7C04 zh-CHT Chinese (Traditional)
0x1004 zh-SG Chinese (Singapore) 0x0004 zh-CHS Chinese (Simplified)
0x1404 zh-MO Chinese (Macao S.A.R.) 0x7C04 zh-CHT Chinese (Traditional)

 

So, okay.  That behavior and classification system seems perfectly defensible and understandable... 

  
...But it's not good enough.

Think about it this way: Wisdom,  the consumer of this framework result,  is already working in a language that is not his own, just to read the blog and navigate the list.  I am truly grateful he makes the effort.

Why should he have to realize that the English-speaking world refers to his country as "People's Republic of China", when his motherland-centered heart envisions it as "China", even in English? 

He knows he's supposed be navigating this list according to a sort-order based on an alphabetization that is also not his -- in which "P" is nowhere near "C".   Why should he look near "P"?

I want to point out that Wisdom didn't even ask "why isn't the value in the list".  He asked: "why can't I find  the value in the list."  It's there, but he can't find it.  And it's not his fault, even though he implicitly accepted that the problem was his.

I value Wisdom's comments, and those of the other members of my team.  So, while I "get" the value of the .NET framework generalized approach here, I'm going to include the following, along with the Bangladesh-adding coding example quoted above, in the original code:

//LSN
// add to standard-generated
// <option value="cn">People's Republic of China</option>


dic.Add("China", "cn");
col.Add("China");

Sure, it's a hack.  So what?

No perfect balance

When you work in a globally-dispersed team, as I do, you often have to balance two goals:

  1. Everybody needs to share some common vocabulary and frame of reference.  Consensus isn't real if every member of the group has a different idea of what was discussed and agreed on. 

    From a technical point of view, frameworks like .NET provide a common vocabulary in the guise of a set of shared classes and libraries, with known capabilities, features, and functions.  The fact that the names of the classes are English-based, in many if not all cases, doesn't prevent non-English-speakers from understanding the usage of the classes, although it may make it more difficult for non-English-speakers from guessing at the usage of the classes.

    As the team develops its own libraries, this common vocabulary widens.  The team's "shared muscles" develop strength and agility.

    From a non-technical point of view, the team starts with a few shared cultural assumptions, such as: "We are all software developers" and "We all work for the same company", and it slowly develops its own culture, based on shared jokes, memories, experiences, conversations, and hours.

  2. Everybody needs to respect some boundaries based on cultural disparity.  Because my team has both US and China members, it's not okay for any of us to assume availability of team resources based on our own cultural calendar and holiday schedule. Both US and Chinese team members sometimes generalize about "Americans", but if we had Canadian or Mexican team members, they might take exception. 

    From a technical point of view, frameworks like .NET provide a way to localize user interfaces and feedback mechanisms, calendars, and so on, to express an application's respect for each user's cultural frame of reference.

    From a non-technical point of view, we make all kinds of personal accommodations, from crazy working hours to fine-tuned expectations of communications style.

 

As goals, the drive-to-consensus and respect-for-differences often undercut each other. 

We all do the best we can. So does the .NET Framework, I guess.

There's no moral here.


Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Walkthrough: Integrated authentication and your report procedures

August 21, 2008 20:10 by LSN

Hi everybody,

It's been more than a month since my last blog-confession.  Life is totally getting in the way here, in the form of new job responsibilities and yet-another-move.

I have promised three nice people in the last 24 hours that I would address their SQL RS questions here as soon as I can. I hope they don't get annoyed, I'm bumping another question up to the head of the line... and I really will post more regularly, soon, I promise. 

A co-worker here at EC|Wise had a question, and his question pointed out to me something that, while it should be obvious in RS, and while it actually is easy, just... isn't... obvious at all...

... so it's a good item to highlight here.  (With any luck at least one of the three nice people from the past 24 hours' worth of questions will be helped, too!)

Here's the scenario:

You use integrated authentication in your intranet site, and you create RS reports to analyze the data on that site. 

Normally, your ASP.NET code passes a parameter with the current user identity to your stored procedures, to limit the data displayed to any individual.  Your database maintains tables of user privilege and role.  So how do you make sure that the user sees only the correct data in your RS reports?

The simple answer is "Same way as you do in your ASP.NET application.  You pass a parameter with the user identity, which Reporting Services exposes very nicely for you in the form of the global variable User!UserID".

Expression dialog shows report-global variable set, including User!UserID

I suppose it's possible that some people don't even know that the Globals exist at all.  Hence the screenshot above. 

But it's more likely that the not-obvious part is how to supply those globals as arguments to your stored procedure code, just as you would from ASP.NET.  Hence the walkthrough below.

Ready?

1. The sample report setup

My co-worker's actual report involves some cascading report parameters, each one of which is fed by a stored procedure.  Each stored procedure shows up as a separate dataset for the report, and these datasets supply the picklists for limiting report data. 

What he needs to do is pass the user identity to the stored procedure that populates the first  picklist in the cascade, which in turn will limit the data in all the other picklists. 

There are many other possible scenarios -- he might need to pass the user id to every stored procedure, to filter each picklist separately.  That's fine, they would all work the same way.

In my example, I'll use a simple report from AdventureWorks data, and I'll drive the main body of the report by a stored procedure, which gets a list of cities by StateProvinceID.  While this procedure happens to use a picklist parameter as well, I'll actually pass User!UserID to the main stored procedure, driving the report data.  This will make the whole thing easy to see.

So let's consider the "main" stored procedure, shown in the following screenshot:

Stored procedure receives the identity value

 

... yeah, it's not much to look at. 

This sproc has one parameter that is tied to a report parameter (StateProvinceID) and the other is tied to our user's identity, even though the procedure doesn't do much with the user except send it back for display purposes.

We have a second dataset feeding our picklist parameter in this report.  We fill this dataset using a simple SELECT  Name, StateProvinceID FROM  Person.StateProvince,  nothing fancy.

OK so far?

Quick review: driving the picklist with a parameter

Here's our parameter setup; you can see that it has a NULL default value, meaning that the user has to pick a StateProvince (by Name, in this case) to actually run the report.

StateProvinceID parameter driven by query

Report layout

Our report layout really has one simple job: demonstrate what the current value of User!UserID happens to be, and that the value sent to the stored procedure is correct (the same as User!UserID.

Sample report layout

... still not very exciting.  You can see that, in the header, I show the report-global variable, and in the table, I show the value coming back from the stored procedure.

Whew. that was a long wind-up.  Where's the beef?

2. The "meat" of the work: passing a report variable to the stored procedure

I've set my main data set for the report to use my stored procedure (note Command type):

Set up the dataset to read from a stored procedure.

Wait just a gosh-darned moment. 

How did I get to the dialog in the last screen shot?  Oh, yes.  Another not-obvious thing -- use the ellipsis button on the Report Designer data tab:

Back to configuring the stored procedure 

Now go to the Parameters tab in this dialog.  See?  

Bind the parameters

You can even use intellisense (sort of) to build up an expression.  You'll be using the same dialog as you see in the first screen shot in this post.

3. The result

That's it.  Stand back and watch it work... whether in your IDE for testing or in Report Manager (or otherwise requested from ReportServer)...

Sample report as viewed in Report Manager

As they used to say on the cereal commercials, "Stays crunchy, even in millk".


Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5