如何显示脚本名称,如 Latin、Cyrillic、Arabic?

使用 Intl.DisplayNames API 将脚本代码转换为任何语言下可读的书写系统名称。

简介

脚本是一种书写系统。Latin(拉丁)是英语、法语和西班牙语所用的脚本。Cyrillic(西里尔)是俄语、保加利亚语和乌克兰语所用的脚本。Arabic(阿拉伯)是阿拉伯语、波斯语和乌尔都语所用的脚本。脚本与语言不同,因为同一种语言可以用多种脚本书写。例如,塞尔维亚语既可以用 Cyrillic,也可以用 Latin。塞尔维亚的用户可以选择自己喜欢的脚本。

在构建语言选择器、字体选择器或文本输入控件时,需要显示脚本名称,以便用户识别书写系统。Intl.DisplayNames API 可以将脚本代码转换为本地化、易于理解的名称,无需维护翻译表。

理解脚本与语言

脚本和语言不是一回事。语言是人们说的内容,脚本是人们书写的方式。

英语是一种语言,Latin 是一种脚本。英语使用 Latin 脚本,但还有许多其他语言也使用 Latin,包括西班牙语、法语、德语、越南语和土耳其语。

塞尔维亚语是一种可以用两种脚本书写的语言。用 Cyrillic 书写的塞尔维亚语是“Српски”,用 Latin 书写的是“Srpski”。两者表示相同的语言,词汇和语法也相同,区别仅在于书写系统。

中文有两种常见的脚本变体。简体中文使用简化汉字,繁体中文使用繁体汉字。相同的句子在不同脚本下会有不同的表现形式。

这种区分在构建界面时非常重要。塞尔维亚用户可能更喜欢使用西里尔文,而不是拉丁文。中国用户则需要在简体和繁体之间进行选择。你的界面需要显示文字脚本名称,以便用户做出选择。

硬编码脚本名称的问题

你可以创建一个查找表,将脚本代码映射到脚本名称。

const scriptNames = {
  Latn: "Latin",
  Cyrl: "Cyrillic",
  Arab: "Arabic",
  Hans: "Simplified Chinese",
  Hant: "Traditional Chinese"
};

console.log(scriptNames.Latn);
// "Latin"

这种方法只适用于英语用户。说其他语言的用户会看到他们可能无法理解的英文脚本名称。你需要为支持的每种语言都提供翻译。

const scriptNames = {
  en: {
    Latn: "Latin",
    Cyrl: "Cyrillic",
    Arab: "Arabic"
  },
  es: {
    Latn: "latino",
    Cyrl: "cirílico",
    Arab: "árabe"
  },
  fr: {
    Latn: "latin",
    Cyrl: "cyrillique",
    Arab: "arabe"
  }
};

这种方式很快就变得难以维护。每增加一种新语言,就需要一整套新的翻译。每增加一种新脚本,每种语言都要新增条目。你需要一个更好的解决方案。

使用 Intl.DisplayNames 获取脚本名称

Intl.DisplayNames 构造函数会创建一个格式化器,将脚本代码转换为可读名称。你需要指定 locale,并将 type 设置为 "script"

const names = new Intl.DisplayNames(["en"], { type: "script" });
console.log(names.of("Latn"));
// "Latin"

第一个参数是 locale 标识符数组。第二个参数是 options 对象,其中 type: "script" 告诉格式化器你需要脚本名称。of() 方法接收脚本代码并返回其名称。

脚本代码遵循 ISO 15924 标准。每个脚本都有一个四个字母的代码,第一个字母大写,后面三个字母小写。拉丁文是 Latn,西里尔文是 Cyrl,阿拉伯文是 Arab

const names = new Intl.DisplayNames(["en"], { type: "script" });

console.log(names.of("Latn"));
// "Latin"

console.log(names.of("Cyrl"));
// "Cyrillic"

console.log(names.of("Arab"));
// "Arabic"

console.log(names.of("Hani"));
// "Han"

console.log(names.of("Hira"));
// "Hiragana"

console.log(names.of("Kana"));
// "Katakana"

格式化器会自动处理所有脚本名称翻译的复杂性。你只需要提供脚本代码即可。

常用文字代码

ISO 15924 标准为 160 多种文字定义了代码。以下是最常用的代码。

const names = new Intl.DisplayNames(["en"], { type: "script" });

console.log(names.of("Latn"));
// "Latin"

console.log(names.of("Cyrl"));
// "Cyrillic"

console.log(names.of("Arab"));
// "Arabic"

console.log(names.of("Hebr"));
// "Hebrew"

console.log(names.of("Deva"));
// "Devanagari"

console.log(names.of("Thai"));
// "Thai"

console.log(names.of("Hani"));
// "Han"

console.log(names.of("Hans"));
// "Simplified Han"

console.log(names.of("Hant"));
// "Traditional Han"

console.log(names.of("Hang"));
// "Hangul"

console.log(names.of("Hira"));
// "Hiragana"

console.log(names.of("Kana"));
// "Katakana"

console.log(names.of("Beng"));
// "Bengali"

console.log(names.of("Grek"));
// "Greek"

Latin 覆盖大多数西欧语言。Cyrillic 覆盖俄语、保加利亚语、乌克兰语及其他斯拉夫语。Arabic 覆盖阿拉伯语、波斯语和乌尔都语。Han 覆盖中文,其中 Hans 表示简体中文,Hant 表示繁体中文。Hangul 覆盖韩语。Hiragana 和 Katakana 是日语文字。

在不同本地化环境下显示文字名称

文字名称会根据显示语言环境进行本地化。可以通过为不同语言环境创建格式化器,查看不同语言下的名称。

const enNames = new Intl.DisplayNames(["en"], { type: "script" });
const esNames = new Intl.DisplayNames(["es"], { type: "script" });
const frNames = new Intl.DisplayNames(["fr"], { type: "script" });
const jaNames = new Intl.DisplayNames(["ja"], { type: "script" });

console.log(enNames.of("Latn"));
// "Latin"

console.log(esNames.of("Latn"));
// "latino"

console.log(frNames.of("Latn"));
// "latin"

console.log(jaNames.of("Latn"));
// "ラテン文字"

每个格式化器会返回其显示语言环境下的文字名称。这可以自动处理文字名称翻译的所有复杂性。

同样的模式适用于任何文字代码。

const enNames = new Intl.DisplayNames(["en"], { type: "script" });
const deNames = new Intl.DisplayNames(["de"], { type: "script" });
const zhNames = new Intl.DisplayNames(["zh"], { type: "script" });

console.log(enNames.of("Cyrl"));
// "Cyrillic"

console.log(deNames.of("Cyrl"));
// "Kyrillisch"

console.log(zhNames.of("Cyrl"));
// "西里尔文"

console.log(enNames.of("Arab"));
// "Arabic"

console.log(deNames.of("Arab"));
// "Arabisch"

console.log(zhNames.of("Arab"));
// "阿拉伯文"

格式化器会自动应用每种语言的正确语言规范。

为语言变体构建文字选择器

某些语言允许用户选择不同的文字。例如,塞尔维亚语可以用 Cyrillic 或 Latin 书写,中文可以用简体或繁体字符。你需要展示这些选项,方便用户选择。

function getScriptOptions(language, userLocale) {
  const names = new Intl.DisplayNames([userLocale], { type: "script" });

  if (language === "sr") {
    return [
      { code: "Cyrl", name: names.of("Cyrl") },
      { code: "Latn", name: names.of("Latn") }
    ];
  }

  if (language === "zh") {
    return [
      { code: "Hans", name: names.of("Hans") },
      { code: "Hant", name: names.of("Hant") }
    ];
  }

  return [];
}

console.log(getScriptOptions("sr", "en"));
// [
//   { code: "Cyrl", name: "Cyrillic" },
//   { code: "Latn", name: "Latin" }
// ]

console.log(getScriptOptions("zh", "en"));
// [
//   { code: "Hans", name: "Simplified Han" },
//   { code: "Hant", name: "Traditional Han" }
// ]

console.log(getScriptOptions("zh", "es"));
// [
//   { code: "Hans", name: "han simplificado" },
//   { code: "Hant", name: "han tradicional" }
// ]

此函数会以用户界面语言返回文字选项。塞尔维亚用户会看到 Cyrillic 和 Latin 文字选项。中文用户会看到简体和繁体选项。名称会以用户能理解的语言显示。

显示包含文字信息的完整语言环境标识符

语言环境标识符可以包含文字代码,用于区分不同的书写系统。格式为 language-script-region,例如 sr-Cyrl-RS 表示在塞尔维亚用 Cyrillic 书写的塞尔维亚语,或 zh-Hans-CN 表示在中国使用简体中文。

在显示这些 locale 标识符时,提取 script 代码并将其转换为可读名称。

function parseLocaleWithScript(locale) {
  const parts = locale.split("-");

  if (parts.length < 2) {
    return null;
  }

  const [language, script] = parts;

  if (script.length === 4) {
    return {
      language,
      script: script.charAt(0).toUpperCase() + script.slice(1).toLowerCase()
    };
  }

  return null;
}

function formatLocaleWithScriptName(locale, displayLocale) {
  const parsed = parseLocaleWithScript(locale);

  if (!parsed) {
    return locale;
  }

  const languageNames = new Intl.DisplayNames([displayLocale], {
    type: "language"
  });
  const scriptNames = new Intl.DisplayNames([displayLocale], { type: "script" });

  const languageName = languageNames.of(parsed.language);
  const scriptName = scriptNames.of(parsed.script);

  return `${languageName} (${scriptName})`;
}

console.log(formatLocaleWithScriptName("sr-Cyrl", "en"));
// "Serbian (Cyrillic)"

console.log(formatLocaleWithScriptName("sr-Latn", "en"));
// "Serbian (Latin)"

console.log(formatLocaleWithScriptName("zh-Hans", "en"));
// "Chinese (Simplified Han)"

console.log(formatLocaleWithScriptName("zh-Hant", "en"));
// "Chinese (Traditional Han)"

console.log(formatLocaleWithScriptName("sr-Cyrl", "es"));
// "serbio (cirílico)"

这种模式通过将语言名称与书写系统名称组合,使 locale 标识符更易于理解。用户会看到“塞尔维亚语(西里尔字母)”而不是“sr-Cyrl”。

使用书写系统名称创建字体选择器

字体选择界面通常会根据字体支持的书写系统进行分组。你需要显示书写系统名称,以便用户了解每种字体支持哪些书写系统。

function createFontOptions() {
  const fonts = [
    {
      name: "Arial",
      scripts: ["Latn", "Cyrl", "Grek", "Hebr", "Arab"]
    },
    {
      name: "Noto Sans CJK",
      scripts: ["Hans", "Hant", "Hira", "Kana", "Hang"]
    },
    {
      name: "Noto Sans Devanagari",
      scripts: ["Deva"]
    }
  ];

  const names = new Intl.DisplayNames(["en"], { type: "script" });

  return fonts.map((font) => ({
    name: font.name,
    scripts: font.scripts.map((code) => names.of(code))
  }));
}

console.log(createFontOptions());
// [
//   {
//     name: "Arial",
//     scripts: ["Latin", "Cyrillic", "Greek", "Hebrew", "Arabic"]
//   },
//   {
//     name: "Noto Sans CJK",
//     scripts: ["Simplified Han", "Traditional Han", "Hiragana", "Katakana", "Hangul"]
//   },
//   {
//     name: "Noto Sans Devanagari",
//     scripts: ["Devanagari"]
//   }
// ]

这会以可读形式创建包含所支持书写系统的字体列表。用户可以根据所需的书写系统选择字体。

按书写系统显示可用输入法

操作系统和浏览器为不同的书写系统提供输入法。例如,日语输入法可以将拉丁字符转换为平假名、片假名或汉字。中文输入法可以将拼音转换为简体或繁体汉字。你可以用书写系统名称显示可用的输入法。

function getInputMethods(userLocale) {
  const inputMethods = [
    { id: "latin-ime", script: "Latn" },
    { id: "japanese-ime", script: "Hira" },
    { id: "chinese-pinyin-simplified", script: "Hans" },
    { id: "chinese-pinyin-traditional", script: "Hant" },
    { id: "korean-ime", script: "Hang" },
    { id: "arabic-ime", script: "Arab" },
    { id: "hebrew-ime", script: "Hebr" }
  ];

  const names = new Intl.DisplayNames([userLocale], { type: "script" });

  return inputMethods.map((method) => ({
    id: method.id,
    name: names.of(method.script)
  }));
}

console.log(getInputMethods("en"));
// [
//   { id: "latin-ime", name: "Latin" },
//   { id: "japanese-ime", name: "Hiragana" },
//   { id: "chinese-pinyin-simplified", name: "Simplified Han" },
//   { id: "chinese-pinyin-traditional", name: "Traditional Han" },
//   { id: "korean-ime", name: "Hangul" },
//   { id: "arabic-ime", name: "Arabic" },
//   { id: "hebrew-ime", name: "Hebrew" }
// ]

console.log(getInputMethods("ja"));
// [
//   { id: "latin-ime", name: "ラテン文字" },
//   { id: "japanese-ime", name: "ひらがな" },
//   { id: "chinese-pinyin-simplified", name: "簡体字" },
//   { id: "chinese-pinyin-traditional", name: "繁体字" },
//   { id: "korean-ime", name: "ハングル" },
//   { id: "arabic-ime", name: "アラビア文字" },
//   { id: "hebrew-ime", name: "ヘブライ文字" }
// ]

这会以用户的语言显示输入法名称。例如,界面为英文时用户会看到“Hiragana”,界面为日文时会看到“ひらがな”。

理解书写系统代码的大小写规范

书写系统代码遵循特定的大小写模式。首字母大写,其余三个字母小写。Latn 是正确的。LATNlatnLaTn 都不是标准写法。

of() 方法可以接受任意大小写的书写系统代码。

const names = new Intl.DisplayNames(["en"], { type: "script" });

console.log(names.of("Latn"));
// "Latin"

console.log(names.of("LATN"));
// "Latin"

console.log(names.of("latn"));
// "Latin"

console.log(names.of("LaTn"));
// "Latin"

该格式化器能够正确处理所有大小写变体。但采用标准的大小写模式可以让你的代码更易读,并与 ISO 15924 标准保持一致。

处理回退语言环境

Intl.DisplayNames 构造函数接受一个语言环境数组。如果第一个语言环境不可用,格式化器会回退到数组中的下一个语言环境。

const names = new Intl.DisplayNames(["xx-XX", "en"], { type: "script" });
console.log(names.of("Latn"));
// "Latin"

格式化器首先尝试 "xx-XX",如果不存在,则回退到 "en"。这样可以确保即使请求的语言环境不可用,代码也能正常工作。

你可以使用 resolvedOptions() 方法检查格式化器实际使用的是哪个语言环境。

const names = new Intl.DisplayNames(["xx-XX", "en"], { type: "script" });
console.log(names.resolvedOptions().locale);
// "en"

这表明格式化器在回退后解析为英文。

构建多语言内容管理系统

支持多种书写系统的内容管理系统需要显示每条内容可用的书写系统。你可以显示脚本名称,帮助内容编辑选择正确的版本。

function getContentVersions(contentId, userLocale) {
  const versions = [
    { script: "Latn", url: `/content/${contentId}/latn` },
    { script: "Cyrl", url: `/content/${contentId}/cyrl` },
    { script: "Arab", url: `/content/${contentId}/arab` }
  ];

  const names = new Intl.DisplayNames([userLocale], { type: "script" });

  return versions.map((version) => ({
    script: version.script,
    name: names.of(version.script),
    url: version.url
  }));
}

console.log(getContentVersions("article-123", "en"));
// [
//   { script: "Latn", name: "Latin", url: "/content/article-123/latn" },
//   { script: "Cyrl", name: "Cyrillic", url: "/content/article-123/cyrl" },
//   { script: "Arab", name: "Arabic", url: "/content/article-123/arab" }
// ]

这种模式有助于内容编辑看到存在哪些脚本版本,并在它们之间切换。

浏览器支持

带有脚本类型支持的 Intl.DisplayNames API 在所有现代浏览器中都可用。从 2021 年 3 月起,主流浏览器(包括 Chrome、Firefox、Safari 和 Edge)均已支持。

你可以在使用前检查该 API 是否可用。

if (typeof Intl.DisplayNames !== "undefined") {
  const names = new Intl.DisplayNames(["en"], { type: "script" });
  console.log(names.of("Latn"));
} else {
  console.log("Intl.DisplayNames is not supported");
}

对于旧版浏览器,你需要提供回退方案或使用 polyfill。一个简单的回退方式是使用脚本代码到名称的硬编码映射。

function getScriptName(code, locale) {
  if (typeof Intl.DisplayNames !== "undefined") {
    const names = new Intl.DisplayNames([locale], { type: "script" });
    return names.of(code);
  }

  const fallbackNames = {
    Latn: "Latin",
    Cyrl: "Cyrillic",
    Arab: "Arabic",
    Hans: "Simplified Han",
    Hant: "Traditional Han"
  };

  return fallbackNames[code] || code;
}

console.log(getScriptName("Latn", "en"));
// "Latin"

这样可以确保即使在不支持 Intl.DisplayNames 的浏览器中,代码也能正常工作,但会失去自动本地化功能。