整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

「西非漫谈」几内亚比绍的农业

源:环球网

(作者:电子科技大学西非研究中心团队,执笔人:ABIGAIL AKAKPO(电子科技大学公共管理学院、西非研究中心助理),译者:刘佳佳、孟雅琪(公共管理学院)【西非漫谈】2021年第二十一期,总第四十六期。整理:孟雅琪,供稿:赵蜀蓉)

几内亚比绍资源丰富,地理位置优越,适宜多样化的农业生产。不过,几十年来,几内亚比绍的农业和经济变化甚微,经济增长缓慢且不稳定。

排水和土壤

几内亚比绍的沿海地区由沉没式山谷包围,即里亚斯。巴法塔高原的水流向格巴河和科鲁巴尔河(巴法塔位于几内亚比绍中北部,格巴河从中跨过,自东向西流经该地区的北半部,可通向巴法塔市,因此是该地区的首府)。加布平原位于该国的东北部,其天然排水系统为卡谢乌河和格巴河及其支流。塞内加尔河盆地的南部边界是内平原。洪泛区的高海拔使得此地河流蜿蜒,因此,该地区在雨季易遭洪水侵袭。

气候

几内亚比绍总体上属热带气候,受热带辐合区(ITCZ)的影响,这是一条在赤道附近环绕地球的汇聚信风带。有两季:炎热的雨季(通常从6月持续到11月)及旱季。4月和5月是最热的月份,温度可达90华氏度(30摄氏度左右)。

(几内亚比绍比绍温度和雨量示意图,图片来源:commons.wikimedia.org)

植物和动物生活

几内亚比绍有三个生态区,即潮音区、森林茂密的内陆平原和热带稀树草原,是种类繁多的动植物群的家园。在沿海沼泽地,火烈鸟和鹈鹕等水生和河流鸟类很是常见,蛇、鳄鱼和濒临灭绝的海龟等爬行动物也是如此。蜥蜴、瞪羚、羚羊、猴子、猿类、鹦鹉、鬣狗和豹子在平原和林地上繁衍生息。

(图片来源:ifad.org)

农业、林业和渔业

几内亚比绍以农业为主,林业和渔业发展前景良好。本地生产的大米、蔬菜、豆类、木薯、马铃薯、棕榈油和花生等食品也由本地人所消费,

为当地消费而生产的食品包括大米、蔬菜、豆类、木薯(cassava)、马铃薯、棕榈油和花生(peanuts)。牲畜主要有猪、山羊、绵羊、牛和家禽。鱼和虾尤为重要,它们的养殖既用于国内消费,也用于出口。几内亚比绍的森林覆盖率极高,几乎占全国土地面积的五分之三。收获的大部分木材用于国内燃料,也出口少量锯木。长期以来,腰果、棕榈制品、大米、花生、木材和棉花的商业出口对该国的经济至关重要。然而,由于传统刀耕火种的轮作方式,出于政治和军事原因缺乏农业信贷和投资,大量的土地没有用于耕种。几内亚比绍的可持续农业已成为解决这些问题的关键。2008年,在振兴农业特别是水稻生产中直接应对危机成为了优先事项。该国有几个地区适合生产水稻,不过那些土地尚未开垦,又缺乏其他粮食生产地的信息,因此,这些地区的公民依然处在贫困之中。

(图片来源: worldatlas.org)

引进了新的可持续方法后,这些地区的水稻产量现已翻了一番。欧盟还启动了一项资助计划,用于修复该地区的道路,长达300公里,以提高货运效率。这一问题已存在多年,但更多可持续的方法和及欧盟资助计划等项目对于解决该国的气候变化问题也至关重要。

(几内亚比绍的可持续发展农业,图片来源: borgenproject.org)

众所周知,几内亚比绍是一个以农业为基础的国家,不过,其海岸线上也有丰富的鱼类和贝类,与俄罗斯、阿尔及利亚和葡萄牙等国的公司建立了联合捕鱼企业(获得许可的渔业收入占政府收入的40%(1992-96)。另一方面,过度捕捞导致此地捕捞潜力下降,因此实施了欧盟所支持的现代化计划,其中包括配额制度和增加海上巡逻。1996年,几内亚比绍还与其他六个西非国家达成了交叉监测渔区的协议。

几内亚比绍目前是世界上最大的腰果原料生产国之一。十多年来,腰果在几内亚比绍经济中的重要性不断提高,就税收而言,出口总收入的95%来自于腰果,75%的农村家庭也从事腰果生产行业。腰果树早在6世纪就由葡萄牙人带入该国,但直到20世纪80年代中期才得到有效开发,因此,该国现在专注于生腰果的生产和销售。2013年,腰果出口额占该国出口总额的87.7%,这反映了自20世纪90年代末以来该行业的长势。腰果主要在村庄里由农民小规模种植,大约85%的农村人口都依赖腰果种植,因此,因疾病导致的作物歉收会影响数十万公民。对于巴兰塔人这样的非穆斯林族群,腰果生产提供了两种收入来源,即坚果和酒。在腰果价格较低的年份,用苹果汁酿成的腰果酒带来的收入比腰果高。

(巴法塔村的腰果园,树距很近,树下物种多样性低。,照片由Catarino和Monteiro拍摄,图片来源:Researchgate.net)

如今,几内亚比绍可持续农业以教育为其一大特色。Agrisud International等非政府组织与几内亚比绍人民合作以推广更多的可持续方法。同时还与政府合作,以普及这些方法。在国际组织和政府继续支持下,几内亚比绍的农业将不断改善。

农业是几内亚比绍最重要的产业,占国内生产总值 (GDP) 的一半以上。粮食自给自足是几内亚比绍政府的目标,粮食类产品主要有大米、木薯、豆类、土豆、红薯、甘蔗和热带水果。水稻生产面积占耕地面积的30%。畜牧业已从不稳定的政治局势中恢复过来,2001年,牛的数量达到55万头,这一数字对于刚刚超过100万人口的国家来说是很高的。尽管第一个粮食自给自足发展计划是在 1983 年制定的,但在20世纪90年代末,粮食仍然是进口的主体,几十年来腰果的生产和进口也都在增加。

(一些工厂的工人正在处理生腰果,图片来源:uniogbis.unmissions.org)

参考文献:

[if !supportLists](1) African Union. 2014. “Malabo Declaration on Accelerated Agricultural Gorwth and transformation for Shared Prosperity and Improved Livelihoods” available online: http://www.resakss.org/sites/default/files/Malabo%20Declaration%20on%20Agriculture_2014_11%2026.pdf

[if !supportLists](2) African Union. 2018. “African Union launches Africa Agriculture Transformation Scorecard” Press release, https://au.int/en/pressreleases/20180129/african-union-launches-africa-agriculture-transformationscorecard-aats-%E2%80%93

[if !supportLists](3) Green Climate Fund. 2018. “Enhancing Livestock Resilience to Drought in Guinea-Bissau”. Concept Note: BOAD.

[if !supportLists](4) Campos, F. 2016. “Facilitando o financiamento competitivo para o setor do cajú”, Presentation at the 2016 Africa Cashew Alliance conference, Bissau, September 19-22, 2016

[if !supportLists](5) Chivandi, E.; Mukonowenzou, N.; Nyakudya, T.; Erlwanger, K.H. Potential of indigenous fruit-bearing trees to curb malnutrition, improve household food security, income and community health in Sub-Saharan Africa: A review. Food Res. Int. 2015, 76, 980–985.

[if !supportLists](6) FAO. FAO in the 21st CENTURY, Ensuring Food Security in a Changing World. 2011. Available online: http://www.fao.org/docrep/015/i2307e/i2307e.pdf (accessed on 14 September 2017).

[if !supportLists](7) Pretty, J.; Bharucha, Z.P. Sustainable intensification in agricultural systems. Ann. Bot. 2014, 114, 1571–1596.

[if !supportLists](8) Unnevehr, L. Food safety in developing countries: Moving beyond exports. Glob Food Security. 2015, 4, 24–29.

[if !supportLists](9) Tropical Research Institute/Tropical Botanic Garden, Trav. Conde da Ribeira, 9 − 1300-142 – Lisboa − Portugal. University of Porto/Research Centre for Biodiversity and Genetic Resources, Agricultural Campus of Vairão − 4485-601 – Vairão − Portugal.

[if !supportLists](10) National Institute of Studies and Research – Complexo Escolar 14 de Novembro, C.P. 112 Bissau − Guiné-Bissau.

[if !supportLists](11) Piaget Institute/International Center for Research on Epistemology and Transdisciplinary Reflection, Av. João Paulo II, Lote 544-2ª − 1900-726 − Lisboa, Portugal.

[if !supportLists](12) Addisu, W. 2012. The overall Agricultural Development in Guinea Bissau: Read more: https://www.nationsencyclopedia.com/economies/Africa/Guinea-Bissau AGRICULTURE.html#ixzz7Ac5OZjyX

多国内企业正积极开拓国际市场,如Shopee、阿里的Lazada、字节的TikTok、拼多多海外版Temu、以及服装快消领域的Shein等。当国内市场存量业务达到峰值预期时,海外业务成为各公司未来收入增长的主要动力,因此,国际化已成为越来越重要的职业发展方向。

国内IT企业收入天花板: 「10亿X2元X365天=7300亿元」,也就是10亿人口,企业每天赚取用户2元,保持365天,就是单业务增长的营收天花板(大部分业务赚不到2元,用户量也没到10亿)。比如视频如果60元一个月那会员营收天花板就可以这么预估. 甚至比这个还低, 毕竟用户会流失, 拉新也要成本, 运营成本是在递增的。

国际化不仅仅是多语言文案适配这么简单,而是一全套的工程化解决方案。笔者觉得更重要的是「从业人员需要具备全球视野,对多元文化有包容心和敬畏心理,同时知识面要求也较高」。比如,了解SEA、US、UK等常见地区的简写,尊重伊斯兰教的斋月节等习俗。对于服务全球用户的产品来说,对应产品的要求更加复杂,多样性体现在不同的文化习俗差异上,其实即便在庞大的中国内部也存在南北差异。了解的越多越发现这个世界的“多样性”。

概念说明

苹果键盘怎么卖多国?

苹果键盘有很多型号不同型号的布局不一样https://www.apple.com/shop/product/MK2A3J/A/magic-keyboard-japanese

apple-keyboard

那如何模仿苹果造一把可以卖到世界各地的键盘?

  1. 电路板等硬件配件统一生产
  2. 制定三种布局方案(Arabic, Russian, Ukrainian归为一种, Chinese (Zhuyin)和Korean为一种, Japanese为一种),单独开孔
  3. 键帽印刷不同语言的文案
  4. MacOS开发语言输入软件适配不同键盘的语言输入

其中2,3,4都是为产品的全球化服务

全球化=国际化i18n+本地化l10n

https://en.wikipedia.org/wiki/Internationalization_and_localization

globalization

  1. 产品设计和开发部署需要需要考虑国际化i18n
    1. 多语言
    2. 多布局,如阿拉伯语的RTL
    3. 多货币
    4. 全球多地区多机房部署(离用户越近服务体验越好,数据物理存储隔离符合各个国家数据安全要求)
  2. 产品本地化L10N是国际化后的「可选」流程,需要引入「本地化团队」转化和质量验收,再投入本地化市场
    1. 本地化步骤是可选的,如英美产品UI,语言基本一致可互通,本地化投入少
    2. 中文地区等有简体繁体,不同地区用语习惯不一样,也要特别兼顾,如香港的粤语和广州的粤语,在一些用词有区别,像吸管(广州)-饮筒(香港),你可以看https://www.zhihu.com/question/20663233
    3. 阿拉伯语,希伯来语等地区RTL的阅读习惯,对产品改动较大需要特殊适配

国际化

产品面向全球用户,需要做语言适配,针对不同国家地区的用户提供对应语言的版本。本质是「文本替换」,也要考虑文本阅读方向,比如阿拉伯语和希伯来语是从右到左。

可以看下Apple的做法,对不同国家地区提供不同服务

  • Apple US 对应链接 https://www.apple.com/
  • Apple CN 对应链接 https://www.apple.com.cn/
  • Apple HK 对应链接 https://www.apple.com/hk/en/

常见地区语言对应关系可以看 ISO 3166-1(https://baike.baidu.com/item/ISO%203166-1/5269555?fr=ge_ala)

Intl

MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl

浏览器中的 Intl 是一个内置的 JavaScript 对象,用于国际化(Internationalization)支持。它提供了处理日期、时间、数字格式化以及货币和语言的能力,以便网站能够根据用户的语言和地区习惯来显示内容。在 JavaScript 中,您可以使用 Intl 对象来执行以下操作:

  1. 格式化日期和时间:使用 Intl.DateTimeFormat 对象可以格式化日期和时间,并根据用户的地区偏好显示日期和时间。
  2. 格式化数字:使用 Intl.NumberFormat 对象可以格式化数字,并根据用户的地区偏好显示数字。
  3. 处理货币:使用 Intl.NumberFormat 对象结合指定的货币代码可以格式化货币,并根据用户的地区偏好显示货币。
  4. 语言和区域设置信息:使用 Intl.getCanonicalLocales 方法可以获取支持的语言和区域设置信息。关于标点符号,通常在国际化的环境下,标点符号使用英文是比较常见的做法,因为英文标点符号在全球范围内都有通用性,可以避免因为地域差异而引起的误解。在 Intl 对象中,一般不会直接涉及到标点符号的处理,而是主要用于处理日期、时间、数字和货币等格式化需求。

前置知识

语言标识

全地球有N个民族,有的民族有自己的语言, 有的民族用其他国家民族传递过来的语言, 融化吸收然后发展自己的文字。

按照ISO标准(https://zh.m.wikipedia.org/wiki/ISO_639-1),语言可以用大类+小类表示, 比如「zh」就是汉语,是一个大类,而「zh-CN」就是简体中文的缩写, 新加坡华人众多久了就有「zh-SG」, 表示的是新加坡使用的中文,其次还有「zh-HK/zh-TW和zh-Hant/zh-Hans」等等

语言声明是三段式结构 [language]-[script]-[region] , 如zh-Hans-CN表示中国地区简体中文, zh-Hant表示所有中文繁体

Language Code Table(http://www.lingoes.net/zh/translator/langcode.htm)

一起来看下苹果官网是如何适配多国语言的

澳门apple https://www.apple.com/mo/

香港apple

英文 https://www.apple.com/hk/en/

中文 https://www.apple.com/hk/

中国大陆地区apple https://www.apple.com.cn/

台湾apple https://www.apple.com/tw/

新加坡apple https://www.apple.com/sg/

日本apple https://www.apple.com/jp/

可以看到有的是根域名下通过ISO地区的path比如**/hk/**这样来区分的,有的是直接换域名,比如中国大陆地区

文字阅读顺序

按照普通的中文和英文顺序,都是LTR,上到下,都是世界范围通用的

而ar阿拉伯语, ur乌都语, he希伯来语都是特殊的从右到左, 即RTL的一般会通过标签的dir属性标识, 比如下面的解释HTML dir Attribute(https://www.w3schools.com/tags/att_global_dir.asp)

HEBREW是指希伯来语,这是一种在以色列广泛使用的语言,也是犹太教的宗教经典文本的原始语言。它属于阿夫罗亚细亚语系,有着悠久的历史和文化价值。「希伯来语有其独特的书写系统,从右向左书写。」上图的概念很少人普及, 因为非国际化产品不需要多语言, 做需要支持海外业务和全球应用的同学可以多了解下. 传统的英文, 中文简体, 拉丁文等都是上图LATIN的阅读顺序, 如果用上「top, 下bottom, 左left, 右right」代表我们的习惯, 也就是「Z」这样的顺序. 即行到行是从上到下的顺序, 行内阅读顺序是从左到右.

文档流和阅读顺序

即left→right, top→bottom的顺序,有主次分别,left→right的优先级高于top→bottom

而Web标准对其定义是下面这样的

  • left = inline-start
  • right = inline-end
  • top = block-start
  • bottom = block-end

讲个笑话, 古代书籍就是按照 writing-mode: vertical-rl 排版的

joke

布局

content-flows

比如 margin: left 或者 text-align: left 在多语言场景都是不合适的,你的左右不是其他人的左右。

而应该用 margin-inline-starttext-align: start 替代,即inline轴和block轴

// 下面两两相等,请抛弃left/right/top/bottom等属性
// https://web.dev/learn/css/logical-properties/#terminology

margin-left: 1px
margin-inline-start: 1px

margin-right: 1px
margin-inline-end: 1px

margin-top: 1px
margin-block-start: 1px

margin-bottom: 1px
margin-block-end: 1px

text-align: left
text-align: start

text-align: right
text-align: end

max-width: 100px
max-inline-width: 100px
max-inline-size: 150px

max-height: 100px
max-block-width: 100px

padding-left: 1px
padding-inline-start: 1px

padding-top: 1px
padding-block-start: 1px

top: 0.2em;
inset-block-start: 0.2em;

bottom: 0.2em;
inset-block-end: 0.2em;

left: 2px;
inset-inline-start: 2px;

right: 2px;
inset-inline-end: 2px;

border-bottom: 1px solid red;
border-block-end: 1px solid red;

border-bottom-right-radius: 1em;
border-end-end-radius: 1em;

height: 160px;
block-size: 160px;

width: 160px;
inline-size: 160px;

也可以看下面的例子

  • https://codepen.io/web-dot-dev/pen/gOxXOLK
  • https://codepen.io/web-dot-dev/pen/mdMqdOx

如上两个例子通过margin-inline-start等属性,再在html元素上添加 dir: rtl 就可以实现多语言的阅读顺序兼容

由此, 常见的布局也会更新为以下形式,常见的物理盒模型用于尺寸计算, 逻辑盒模型用于国际化处理

盒子模型

writing mode 决定 content-flows

上面写了文档有inline and block flow,对应english的left和right,top和bottom。而 writing-mode 可以修改content-flows,比如下面的值

/* 关键值 */
writing-mode: horizontal-tb;
writing-mode: vertical-rl;
writing-mode: vertical-lr;

可以这么理解 writing-mode: horizontal-tb ,前面的horizontal/vertical是指的inline轴的方向,

https://codepen.io/manfredhu/pen/xxWdpaK

vi和vb

视口宽高viewport在这里也有特殊含义. 比如宽高vw和vh也被取代,用 vi(viewport inline) 和 vb(viewport block)替代

1%宽度=1vw=1vi 1%高度=1vh=1vb

JS的scrollLeft

DOM的API可以通过「Element.scrollLeft」获取到元素滚动的距离,下图是一个实际例子

scrollLeft的rtl

这里在最后做了一个遮罩(绿色边框区域),内部蓝色部分类似一个走马灯,通过overflow:hidden将蓝色高亮部分超出的区域遮住

当蓝色部分滚动到最后,绿色遮罩隐藏,达到一个遮盖,滚动到最后消失的效果,代码如下

const ref = document.querySelector('.tiktok-table__container') // 父节点,蓝色区域
const ref2 = document.querySelector('.tiktok-table__container > table') // 子节点,表格区域
const bufferWidth = 30 // 留一点buffer空间
if (ref && ref2 && ref.clientWidth + ref.scrollLeft >= ref2.clientWidth - bufferWidth) {
  // 滚动到最后隐藏绿色遮罩
  setTableRightMask(false)
} else {
  setTableRightMask(true)
}

但是在RTL下,神奇的事情就发生了,scrollLeft居然是负数

这是因为RTL的实现是通过HTML标签增加属性 dir="rtl” 实现的,会将文档完全翻转过来,所以scrollLeft就会是负数。因为此时(0, 0)这个原点已经是表格右边了

解决方法也很简单,取绝对值呗,这样就忽略了方向的影响

locale

根据ISO标准对全球国家地区进行划分https://en.wikipedia.org/wiki/ISO_3166-2. 如 "US" 表示美国,"CN" 表示中国. 还有常见的如「zh-CN, en-US, en-GB等」

  • CN是国家地区码, 根据国际标准 ISO 3166-1 规定的国家和地区代码。ISO 3166-1 是用于标识国家和地区的国际标准,每个国家或地区都有一个唯一的两字母代码。"CN" 代表中华人民共和国(People's Republic of China),即中国
  • zh-CN是语言地区码, 它通常用于表示中文("zh" 代表中文)以及特定的地区或国家,这里 "CN" 代表中国。"zh" 代表中文,这是根据国际标准 ISO 639-1 规定的语言代码。ISO 639-1 是用于标识语言的国际标准,每个语言都有一个唯一的两字母代码。"zh" 代表中文,但不区分中文的不同方言,如普通话和粤语。
  • zh-Hans-CN 表示中国大陆地区的简体中文,还有"zh-Hans-SG" 可用于表示新加坡的官方简化中文,"zh-Hans-TW" 可用于表示台湾的官方简化中文

Intl. Locale

举个说下Intl API对于locale的定义

const korean = new Intl.Locale('ko', {
  script: 'Kore',
  region: 'KR',
  hourCycle: 'h23',
  calendar: 'gregory',
});

const japanese = new Intl.Locale('ja-Jpan-JP-u-ca-japanese-hc-h12');

console.log(korean.baseName, japanese.baseName);
// Expected output: "ko-Kore-KR" "ja-Jpan-JP"

可以看到Intl. Locale就是把传入的字符串拆解为 [language]-[script]-[region] 的组成.

  • ja:代表语言代码,表示日语(Japanese)
  • Jpan:代表脚本代码,表示使用日语文字(Japanese script)
  • JP:代表地区代码,表示日本(Japan) 这里的 u-ca-japanese 表示unicode calendar也就是日历格式(日本日历与众不同), hc表示hourCycle这里hc-h12表示12小时制. u-ca-japanesehc-h12 的顺序无关, 也就是说如下两种用法完全等价
const japanese = new Intl.Locale('ja-Jpan-JP-u-ca-japanese-hc-h12');
const japanese2 = new Intl.Locale('ja-Jpan-JP-u-hc-h12-ca-japanese');

-u (unicode)可以理解为额外扩展插件, 插件系统支持以下扩展. 如上使用calendar扩展和hourCycle扩展



calendarca (extension)caseFirstkf (extension)collationco (extension)hourCyclehc (extension)numberingSystemnu (extension)numerickn (extension)

calendar:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar)

caseFirst:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/caseFirst)

collation:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/collation)

hourCycle:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle)

numberingSystem:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem)

numeric:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numeric)

语言声明

<html lang="en-US">
<!-- 说明页面语言是美式英文 -->
<p>I felt some <span lang="de">schadenfreude</span>.</p> <!-- lang不是只有html标签才有, 其他标签也可以添加 -->
<a href="/path/to/german/version" hreflang="de" lang="de">Deutsche Version</a> <!-- a标签上可以用lang表示显示的文本的语言, 也可以用hreflang表示跳转页面的语言 -->

language设置

语言属性通常代表语言标识符的主要部分。它标识了用于表达语言的基本信息。例如,在 BCP 47 标准中,语言标识符通常包含了语言的主要代码部分,例如 "en" 代表英语,"es" 代表西班牙语。

const locales = ['en', 'de', 'ja'];
const displayNames = new Intl.DisplayNames('en-US', { type: 'language' });
locales.forEach(locale => console.log(displayNames.of(locale)));

// English
// German
// Japanese

如果已经有如上代码, 再进一步给这些内容添加样式是非常简单的, 我们可以使用CSS的选择器. 如 [lang|="fr"] 或者 :lang(fr)

[lang|="fr"] 选择属性 lang=fr 或者lang属性以fr开头的元素, 如 lang=fr-CA

script设置

脚本属性是语言标识符的可选部分,表示使用的书写系统或文字的风格。这是一个辅助信息,用于更精确地表示特定语言的书写习惯。例如,"Hans" 代表简体中文,"Latn" 代表拉丁文。

script的of支持传入BCP47规范的二字码(https://en.wikipedia.org/wiki/IETF_language_tag), 如zh

const scriptNames = new Intl.DisplayNames('en-US', { type: 'script' });
console.log(scriptNames.of('Hans')); // output:Simplified
console.log(scriptNames.of('Hant')); // output:Traditional
console.log(scriptNames.of('Latn')); // output:Latin

const scriptNames = new Intl.DisplayNames('zh-CN', { type: 'script' });
console.log(scriptNames.of('Hans')); // output:简体
console.log(scriptNames.of('Hant')); // output:繁体
console.log(scriptNames.of('Latn')); // output:拉丁文

region设置

region 的of支持传入https://en.wikipedia.org/wiki/ISO_3166-2 里的国家二字码

const regionNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'region' });
const regionNamesInTraditionalChinese = new Intl.DisplayNames(['zh-Hant'], { type: 'region' });
console.log(regionNamesInEnglish.of('US'));
// Expected output: "United States"
console.log(regionNamesInTraditionalChinese.of('US'));
// Expected output: "美國"

文本

阅读顺序声明

文本的阅读顺序声明

html标签dir属性

可以通过html标签的dir属性设置

<html dir="ltr">
<html dir="rtl">

css属性writing-mode(https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode)

writing-mode: horizontal-tb;
writing-mode: vertical-lr;
writing-mode: vertical-rl;

文本行的布局方向, 以及块的排列方向。如果想作用在整个文档需要设置html标签, 则全局生效

「第一个属性horizontal/vertical指的是行块的排列, 第二个属性则是指文本内容的流向(content flows)」

翻转

形如 「?」 这种符号类,在多语言下是不一样的

比如ar阿拉伯语和ur乌尔都斯语问号是RTL的,即 「؟」(https://zh.m.wiktionary.org/wiki/%D8%9F)是OK的

而he希伯来语是LTR的,即 「?」 是OK的

是不是很神奇?一个问号也能玩出花来

线索管理页-英文和阿拉伯语

常见的需要RTL的语言有下面这些

  1. 「阿拉伯语(AR)」:阿拉伯语是使用 RTL 方向书写的最著名的语言之一。它是中东和北非地区的主要语言,以及伊斯兰教的官方语言。
  2. 「希伯来语(HE)」:希伯来语是犹太人的宗教和文化语言,以及以色列的官方语言。它也是一个使用 RTL 方向书写的语言。
  3. 「波斯语(FA)」:波斯语,也称为法尔西语,是伊朗的官方语言,以及一些中东国家的官方或辅助语言。它使用 RTL 方向书写。
  4. 「乌尔都语(UR)」:乌尔都语是巴基斯坦和印度的官方语言之一,以及一种使用 RTL 方向书写的语言。
  5. 「帕斯图语(PS)」:帕斯图语是阿富汗的官方语言之一,也是使用 RTL 方向书写的语言。
const rtlLangs = [
    'ar', // 阿拉伯语
    'ur', // 巴基斯坦
    'he', // 以色列
    'he-IL', // 希伯来语(以色列)
    'fa-IR', // 波斯语(伊朗)
    'ps' // 帕斯图语
];

多语言文案

l10n本地化的一个比较多工作量的部分是文本的翻译, 一种文本到N种文本的翻译需要引入本地化团队. 技术实现上选择也很多

程序打包嵌入文案

通过 key: text 映射, 比如 t('key') 最后程序跑出来就是text文案, 这种方式不会依赖其他东西, 跟普通网页一样内容都是CDN文件. 缺点是文案做为静态资源需要用户额外获取, 如果处理不好替换错误就展示 key 内容而不是

  • vue i18n:https://kazupon.github.io/vue-i18n/zh/started.html#html
  • react i18n:https://react.i18next.com

以下例子以Vue为例, 配置如「en.json, fr.json」等等的静态配置文案, 打包嵌入CDN的JS文件里

Vue i18n example:https://codesandbox.io/p/sandbox/o7n6pkpwoy?file=%2Fstore.js%3A10%2C14

接口获取

程序运行时通过接口拿文案,可以通过html标签添加query参数 lang=xxx 标记页面语言, 或者cookie标记语言选择

实时翻译替换

加载翻译的脚本, 在切换语言的时候替换掉加载的文本。好处是加载的脚本是当前语言所需要的, 不会有其他语言的冗余. 缺点是依赖一个翻译服务, 如果翻译服务宕机了网页就不能正常访问了

User -> gateway -> SSR -> i18n cache -> read-time translation services(实时翻译服务)

占位符与单复数处理-ICU语法

DevPal - ICU message editor:https://devpal.co/icu-message-editor/?data=I%20have%20%7Bnum%2C%20plural%2C%20one%7B%7Bnumber%7D%20%23%20apple%7D%20other%7B%23%20apples%7D%7D%2Cbut%20it%27s%20too%20small%0A

ICU语法即通用的有if-else逻辑的DSL,如下DSL可以根据传入的值换取不同的表示,常用于国际化业务

I have {num, plural, one{{number} # apple} other{# apples}},but it's too small

Intl. Segmenter 分段器

如果你用过vim一定知道w(word)可以移动到下个单词, 英文里把文本分为单词、句子和段落,同理中文也是

  • 「w」 word 一个单词
  • 「s」 sentence 一个句子
  • 「p」 paragraph 一个段落
const segmenter = new Intl.Segmenter('en-US', { granularity: 'word' });

const text = 'This is a sample text for demonstration purposes.';

// 使用 Segmenter 对文本进行分割
const segments = [...segmenter.segment(text)];
console.log(segments);

// 0: {segment: 'This', index: 0, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 1: {segment: ' ', index: 4, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 2: {segment: 'is', index: 5, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 3: {segment: ' ', index: 7, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 4: {segment: 'a', index: 8, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 5: {segment: ' ', index: 9, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 6: {segment: 'sample', index: 10, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 7: {segment: ' ', index: 16, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 8: {segment: 'text', index: 17, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 9: {segment: ' ', index: 21, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 10: {segment: 'for', index: 22, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 11: {segment: ' ', index: 25, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 12: {segment: 'demonstration', index: 26, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 13: {segment: ' ', index: 39, input: 'This is a sample text for demonstration purposes.', isWordLike: false}
// 14: {segment: 'purposes', index: 40, input: 'This is a sample text for demonstration purposes.', isWordLike: true}
// 15: {segment: '.', index: 48, input: 'This is a sample text for demonstration purposes.', isWordLike: false}

Intl. Segmenter分段器可以把句子, 段落, 文章等按照配置切割为不同的segment数组, 结构类似正则, 有segment属性

再举个例子, 中文语境下「真的」其实是一个词

// 创建分段器,指定语言环境和分段类型为'word'
const segmenter = new Intl.Segmenter(['en', 'zh'], { granularity: 'word' });

// 要分割的字符串
const text = 'Hello世界Hello world';

// 使用分段器分割字符串
const segments = segmenter.segment(text);

// 遍历并打印每个分段的结果
for (const segment of segments) {
    console.log(`Segment: ${segment.segment}, Index: ${segment.index}, IsWordLike: ${segment.isWordLike}`);
}
// Segment: Hello, Index: 0, IsWordLike: true
// Segment: 世界, Index: 5, IsWordLike: true
// Segment: Hello, Index: 7, IsWordLike: true
// Segment:  , Index: 12, IsWordLike: false
// Segment: world, Index: 13, IsWordLike: true
const str = "我真的很强, 强哥的强";
const segmenterJa = new Intl.Segmenter("zh-CN", { granularity: "word" });

const segments = segmenterJa.segment(str);
console.log(Array.from(segments));

// 0: {segment: '我', index: 0, input: '我真的很强, 强哥的强', isWordLike: true}
// 1: {segment: '真的', index: 1, input: '我真的很强, 强哥的强', isWordLike: true}
// 2: {segment: '很', index: 3, input: '我真的很强, 强哥的强', isWordLike: true}
// 3: {segment: '强', index: 4, input: '我真的很强, 强哥的强', isWordLike: true}
// 4: {segment: ',', index: 5, input: '我真的很强, 强哥的强', isWordLike: false}
// 5: {segment: ' ', index: 6, input: '我真的很强, 强哥的强', isWordLike: false}
// 6: {segment: '强', index: 7, input: '我真的很强, 强哥的强', isWordLike: true}
// 7: {segment: '哥', index: 8, input: '我真的很强, 强哥的强', isWordLike: true}
// 8: {segment: '的', index: 9, input: '我真的很强, 强哥的强', isWordLike: true}
// 9: {segment: '强', index: 10, input: '我真的很强, 强哥的强', isWordLike: true}

时间&时区

国际化会有时区划分问题, 时区产生于太阳下地球自转导致的昼夜交替. 而全球不同国家地区当地时间与UTC时间是不一致的. 全球大部分人都可以说自己早上起床, 晚上睡觉. 上下文是通的. 但是这个早上的时间根据UTC来定义是不一样的

GMT和UTC

  • GTM = Greenwich Mean Time,GTM是英国格林威治时间,但是与太阳时偏差较大,已成为历史不再作为标准
  • UTC = 「协调世界时(UTC: Coordinated Universal Time)- 由原子钟提供」

时间的往事--记一次与夏令时的斗智斗勇:https://jiangyixiong.top/2021/05/25/%E6%97%B6%E9%97%B4%E7%9A%84%E5%BE%80%E4%BA%8B%E2%80%94%E2%80%94%E8%AE%B0%E4%B8%80%E6%AC%A1%E4%B8%8E%E5%A4%8F%E4%BB%A4%E6%97%B6%E7%9A%84%E6%96%97%E6%99%BA%E6%96%97%E5%8B%87

GMT 标准时间 全球时区查询:https://time.artjoey.com/cn

通过NTP协议(https://zh.wikipedia.org/wiki/%E7%B6%B2%E8%B7%AF%E6%99%82%E9%96%93%E5%8D%94%E5%AE%9A), 让计算机在全球网络里保持时间一致

「Offset与Timezone」

  • Offset即偏移量,比如中国在东八区,Offset是+08:00:00
  • 而东八区不止包括中国时间,而是一组东西经符合一个区域的集合,比如
东八区 = {CST(中国标准时),SGT(新加坡时间),AWST(澳洲西部标准时)... }

如何获取当前用户的时区信息

// 所在地区的时区标识符, 如 America/New_York
const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log("用户时区偏移:" + timeZone); // 用户时区偏移:Asia/Shanghai

// 获取本地时间与UTC时间偏移值,最小单位是分钟. 如"-480", 表示-8小时. 其中正负表示UTC前后, 如美国东部时间是UTC-5, 中国北京时间是UTC+8
const date = new Date();
const timeZoneOffset = date.getTimezoneOffset();
console.log("时区偏移:" + timeZoneOffset); // 时区偏移:-480

Intl是新的浏览器API, 与Math类似是全局静态对象, 专门用于处理国际化和本地化业务. 其下的DateTimeFormat可以处理时间相关国际化问题

DST

DST (Daylight saving time),日光节约时,夏令时/冬令时等等名称。「它会在每年春天的某一天将时钟向后拨一小时,又在秋天的某一天将时钟向前拨动一个小时。」非国际化业务很少遇到这个情况,主要因为「中国不实行夏令时/冬令时。」

  • 为什么要实行夏令时?一战时德国率先实行,将每年夏天增加1h,冬天较少1h
  • 会产生什么现象?因为是行政约定,每年都可以自由选择某天某时进入夏令时,各国自由发布。IANA会存储(https://www.iana.org/time-zones)同步各国DST,计算机每小时同步时间后会在某一秒发生「突变」,比如1:59到2点的时候突变会1:00
  • 计算机如何表示时间?计算机都有一个unixTime,它表示当前时间距离世界标准时的1970年1月1日0点0分0秒的毫秒数,是一个绝对值,也就是UTC时间
  • 但是不同地区设备会根据本地有一个格式化,将UTC时间转化为本地时间,比如中国在东八区
2021-03-14 01:59:59 GMT-08:00(太平洋标准时间,PST)
2021-03-14T01:59:59.000-08:00(ISO格式表示)
2021-03-14T09:59:59.000Z(转换为UTC时间并以ISO格式表示)

// 下一秒时间突变
2021-03-14 03:00:00 GMT-07:00(太平洋夏令时间,PDT)
2021-03-14T03:00:00.000-07:00(ISO格式表示)
2021-03-14T10:00:00.000Z(转换为UTC时间并以ISO格式表示)
// 原始时间字符串
const timeString = "2021-03-14T09:59:59.000Z";

// 将时间字符串转换为 Date 对象
const date = new Date(timeString);
const pstOutput = date.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour12: false });
console.log(pstOutput); // 3/14/2021, 01:59:59

// 获取时间戳
const timestamp = date.getTime();

// 增加1秒
const newTimestamp = timestamp + 1000;

// 创建新的 Date 对象并格式化为 PDT 时间
const newDate = new Date(newTimestamp);
const pdtOutput = newDate.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour12: false });

console.log(pdtOutput); // 3/14/2021, 03:00:00

时间处理

Dayjs插件

dayjs: https://day.js.org/docs/zh-CN/i18n/i18n

国际化支持 https://github.com/iamkun/dayjs/tree/dev/src/locale

原理:通过拉取多语言文案输出不同的formated日期时间字符串

可以看这个demo

Days of the week:https://codesandbox.io/s/dayjs-dynamic-locale0import-forked-wnk2zq?file=/src/index.js

因我本地系统设置了每周第一天为星期日

Intl API

const date = new Date();
const formattedDate = new Intl.DateTimeFormat('en-US').format(date);
console.log(formattedDate); // 10/29/2023
const formattedDate = new Intl.DateTimeFormat('zh-CN').format(date);
console.log(formattedDate); // 2023/10/29

本地时间输出

// 创建 DateTimeFormat 对象,并指定语言和地区
const dateFormatterCN = new Intl.DateTimeFormat('zh-CN', {
  year: 'numeric',
  month: 'long', // 使用完整的月份名称
  day: 'numeric',
});
console.log(dateFormatterCN.format(new Date('2024-04-28'))); // 2024年4月28日

const dateFormatterUS = new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long', // 使用完整的月份名称
  day: 'numeric',
});
console.log(dateFormatterUS.format(new Date('2024-04-28'))); // April 28, 2024

「Intl. RelativeTimeFormat」相对时间

「Intl.RelativeTimeFormat」 是 JavaScript 中的国际化 API,用于格式化相对时间,例如“1 小时前”或“2 天后”。这个 API 可以根据不同的语言和地区设置,以自然语言的方式呈现相对时间,使应用程序能够更好地适应多语言环境。

const rtf1 = new Intl.RelativeTimeFormat('zh', { style: 'short' });

console.log(rtf1.format(3, 'quarter'));
// Expected output: "3个季度后"

console.log(rtf1.format(-1, 'day'));
// Expected output: "1天前"

const rtf2 = new Intl.RelativeTimeFormat('jp', { numeric: 'auto' });

console.log(rtf2.format(2, 'day'));
// Expected output: "后天"

我们知道中文语境是一万以上可以缩写为1万, 或者是 1 0000. 也就是4位数字. 比如 1 2345 6789或者1’2345’6789(’是万位分隔符)可以一眼看出来是一亿两千三百四十五万六千七百八十九. 而如果是123, 456, 789可能很多人会愣很久重新数才知道是多少. 但是现在很多银行APP都在推跟欧美一样的属于后者的千位分隔符. 可以看这篇讨论觉得写的在理

设计产品时,你是如何掉入从众的陷阱中的?– 人人都是产品经理:https://www.woshipm.com/pd/1500589.html)

类似以上例子可以再看下面的举例, 可以发现在德语和法语下, 千分位分隔符分别是.和 (空格)

const number = 1234567.89;
const formattedNumber = new Intl.NumberFormat('zh-CN').format(number);
console.log(formattedNumber); // 1,234,567.89

const number = 1234567.89;
const formattedNumber = new Intl.NumberFormat('en-US').format(number);
console.log(formattedNumber); // 1,234,567.89

const number = 1234567.89;
const formattedNumber = new Intl.NumberFormat('de-DE').format(number);
console.log(formattedNumber); // 1.234.567,89

const number = 1234567.89;
const formattedNumber = new Intl.NumberFormat('fr-FR').format(number);
console.log(formattedNumber); // 1 234 567,89

单复数

英文复数是要加s的, 比如apples

const numbers = [1, 2, 5, 10, 100];
for (const number of numbers) {
  const pluralRules = new Intl.PluralRules('en-US'); // 使用英语环境
  const pluralForm = pluralRules.select(number);

  console.log(`In English, ${number} item${pluralForm !== 'one' ? 's' : ''}.`);
}
// In English, 1 item.
// In English, 2 items.
// In English, 5 items.
// In English, 10 items.
// In English, 100 items.

再比如顺序, 第一第二第三, 英文分别为 first, second, third, fourth, fifth. 聪明的你一定发现规律了. 除了123后面就是数字+th. 简写是1st 2nd. 根据下表可以发现规律

  1. 1 → st, 后面除了11外. 21-91都是21st, 91st这种
  2. 2→ nd, 后面除了12外. 22-92都是22nd, 92nd这种
  3. 3 → rd, 后面除了13外. 23-93都是23rd, 93rd这种
  4. 其他都是补th

数字英文第N1One1st2Two2nd3Three3rd4Four4th10Ten10th11Eleven11th12Twelve12th13Thirteen13th20Twenty20th21Twenty-one21st30Thirty22nd31Thirty-one21st100One hundred100th

const enOrdinalRules = new Intl.PluralRules("en-US", { type: "ordinal" });

const suffixes = new Map([
  ["one", "st"],
  ["two", "nd"],
  ["few", "rd"],
  ["other", "th"],
]);
const formatOrdinals = (n) => {
  const rule = enOrdinalRules.select(n);
  const suffix = suffixes.get(rule);
  return `${n}${suffix}`;
};

formatOrdinals(0); // '0th'
formatOrdinals(1); // '1st'
formatOrdinals(2); // '2nd'
formatOrdinals(3); // '3rd'
formatOrdinals(4); // '4th'
formatOrdinals(11); // '11th'
formatOrdinals(21); // '21st'
formatOrdinals(42); // '42nd'
formatOrdinals(103); // '103rd'

数字格式化

整数分隔和小数分隔

常见的整数分隔符号有千分位分隔, 比如 1000,000 也有万位分隔比如 1000 0000 . 不同语言不一样

常见的小数分隔符号 . , 比如 1000.00 . 不同语言不一样

const number = 1234567.89;
// 格式化为默认数字格式
const formattedNumber = new Intl.NumberFormat().format(number);
console.log(formattedNumber); // 输出: 1,234,567.89
// 格式化为指定语言环境的数字格式
const formattedNumberDE = new Intl.NumberFormat('de-DE').format(number);
console.log(formattedNumberDE); // 输出: 1.234.567,89
// 格式化为指定语言环境的数字格式
const formattedNumberFR = new Intl.NumberFormat('fr-FR').format(number);
console.log(formattedNumberFR); // 输出: 1 234 567,89
const formattedNumberCN = new Intl.NumberFormat('zh-CN').format(number);
console.log(formattedNumberCN)  // 输出: 1,234,567.89

也可以通过参数配置控制小数部分最多/最少有多少位

const number = 1234567.89123;
const formattedNumber = new Intl.NumberFormat('en-US', {
  style: 'decimal', // 可选 'decimal' 表示常规数字格式
  maximumFractionDigits: 3, // 小数部分最多显示三位
}).format(number);
console.log(formattedNumber); // 输出: 1,234,567.891

百分比

正常百分比是0-100数字+%, 但是法语环境百分比符号习惯是 '% '而不是'%', 多了一个空格

const percentage = 0.75;
// 使用默认语言环境
const formattedPercentageDefault = new Intl.NumberFormat('fr-FR', {
  style: 'percent'
}).format(percentage);
console.log(formattedPercentageDefault); // 输出: '75 %'
// 使用指定语言环境
const formattedPercentageFR = new Intl.NumberFormat('fr-FR', {
  style: 'percent',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
}).format(percentage);
console.log(formattedPercentageFR); // 输出: '75,00 %'
// 使用默认语言环境
const formattedPercentageUS = new Intl.NumberFormat('en-US', {
  style: 'percent'
}).format(percentage);
console.log(formattedPercentageUS); // 输出: '75%'
// 使用指定语言环境
const formattedPercentageCN = new Intl.NumberFormat('zh-CN', {
  style: 'percent',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
}).format(percentage);
console.log(formattedPercentageCN); // 输出: '75.00%'

缩写

console.log(new Intl.NumberFormat('en-US', { notation: "compact" , compactDisplay: "short", maximumFractionDigits: 2 }).format(987654321)) // 987.65M
console.log(new Intl.NumberFormat('zh-CN', { notation: "compact" , compactDisplay: "short", maximumFractionDigits: 2 }).format(987654321)) // 9.88亿

货币

货币符号

比如人民币是 ¥ , 美元是 $ , 欧元 , 英镑 £

new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).formatToParts().filter(i => i.type === 'currency')[0].value // '$'
new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).formatToParts().filter(i => i.type === 'currency')[0].value // '¥'
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).formatToParts().filter(i => i.type === 'currency')[0].value // '€'

货币格式化

用常见的几个经济体和身边用的多的case举例说明, 注意看输出

// 美元 $是美元符号
const numberUSD = 123456789.12;
const formattedNumberUSD = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(numberUSD);
console.log(formattedNumberUSD); // $123,456,789.12

// 人民币 ¥是人民币符号
const numberCNY = 123456789.12;
const formattedNumberCNY = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(numberCNY);
console.log(formattedNumberCNY); // ¥123,456,789.12

// 欧元 €是欧元符号
const numberEUR = 123456789.12;
const formattedNumberEUR = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(numberEUR);
console.log(formattedNumberEUR); // 123.456.789,12 €

// 日元
const numberJPY = 123456789.12;
const formattedNumberJPY = new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(numberJPY);
console.log(formattedNumberJPY); // ¥123,456,789

// 英镑 £是英镑符号
const numberGBP = 123456789.12;
const formattedNumberGBP = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(numberGBP);
console.log(formattedNumberGBP); // £123,456,789.12

// 港币
const numberHKD = 123456789.12;
const formattedNumberHKD = new Intl.NumberFormat('zh-HK', { style: 'currency', currency: 'HKD' }).format(numberHKD);
console.log(formattedNumberHKD); // HK$123,456,789.12

// 韩元
const numberKRW = 123456789.12;
const formattedNumberKRW = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(numberKRW);
console.log(formattedNumberKRW); // ₩123,456,789.12

货币的兼容性兜底可以用 Number.prototype.toLocaleString 实现, 也可以用formatjs提供的polyfill

// 美元 $是美元符号
const numberUSD = 123456789.12;
const formattedNumberUSD = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(numberUSD);
const formatttdNumberUSDByLocaleString = Number(numberUSD).toLocaleString('en-US', { style: 'currency', currency: 'USD' });
console.log(formattedNumberUSD); // $123,456,789.12
console.log(numberUSD.toLocaleString()) // 123,456,789.12
console.log(formatttdNumberUSDByLocaleString) // $123,456,789.12

货币单位显示

比如美国是美元, 中国有人民币. 可以直接格式化出来

currencyNames = new Intl.DisplayNames(["zh-Hans"], { type: "currency" });
console.log(currencyNames.of("USD")); // "美元"
console.log(currencyNames.of("EUR")); // "欧元"
console.log(currencyNames.of("TWD")); // "新台币"
console.log(currencyNames.of("CNY")); // "人民币"

currencyNames = new Intl.DisplayNames(["zh-Hant"], { type: "currency" });
console.log(currencyNames.of("USD")); // "美元"
console.log(currencyNames.of("EUR")); // "歐元"
console.log(currencyNames.of("TWD")); // "新台幣"
console.log(currencyNames.of("CNY")); // "人民幣"

排序&列表

Intl. Collator

常见的电话本, 地址簿排序. 不同语言因为字母转换后排序不一致. 「Intl.Collator」 是 JavaScript 的国际化 API 之一,用于字符串比较和排序,以便在多语言环境中执行正确的排序操作。它允许你创建一个 「Collator」 对象,用于根据特定语言和区域设置执行字符串比较和排序,考虑到不同语言的差异。

console.log(['Z', 'a', 'z', 'ä'].sort(new Intl.Collator('de').compare));
// Expected output: Array ["a", "ä", "z", "Z"]
console.log(['Z', 'a', 'z', 'ä'].sort(new Intl.Collator('sv').compare));
// Expected output: Array ["a", "z", "Z", "ä"]
console.log(['Z', 'a', 'z', 'ä'].sort(new Intl.Collator('de', { caseFirst: 'upper' }).compare));
// Expected output: Array ["a", "ä", "Z", "z"]

//创建一个Intl.Collator对象
const collator = new Intl.Collator('en-US', { sensitivity: 'base', usage: 'sort' });

// 可以看出以下输出是按照拼音排序, guang jin mei ming tian yang
console.log(['今','天','阳','光', '明', '媚'].sort(new Intl.Collator('zh').compare)); // ['光', '今', '媚', '明', '天', '阳']

可以发现 options可以传递参数usage和sensitivity, 有如下取值

  1. 「排序方式 (usage)」: 「usage」 选项指定排序的目的
  2. 'sort':用于排序。
  3. 'search':用于搜索操作,通常不区分大小写。
  4. 「敏感性 (sensitivity)」: 「sensitivity」 选项指定字符串比较的敏感性级别
  5. 'base':基本敏感性,不区分重音符号。
  6. 'accent':考虑重音符号,但不区分大小写。
  7. 'case':区分大小写,同时考虑重音符号。
  8. 'case':区分大小写,但不考虑重音符号。
  9. 大小写 (caseFirst)
  10. "upper":表示大写字母(uppercase)在排序中优先考虑。这意味着排序会先考虑所有大写字母,然后再考虑小写字母。在 「caseFirst: "upper"」 情况下,大写字母会排在小写字母之前。
  11. "lower":表示小写字母(lowercase)在排序中优先考虑。这意味着排序会先考虑所有小写字母,然后再考虑大写字母。在 「caseFirst: "lower"」 情况下,小写字母会排在大写字母之前。
  12. "false":表示不指定大写字母和小写字母的排序顺序,它们会一起排序,不区分大小写。
  13. ignorePunctuation: boolean, 表示是否忽略标点符号

有如下的应用方式

字符串比较

const collator = new Intl.Collator('en-US', { sensitivity: 'base', usage: 'sort' }); //创建一个Intl.Collator对象
const result = collator.compare('apple', 'Banana');
console.log(result); // 根据配置输出 -1(apple 在 Banana 前面)

数组排序

我们知道英文字母默认按照ASCII排序, 而如果需要AaBb这样排序只能自己写排序回调


// 创建一个自定义Collator对象
const customCollator = new Intl.Collator('en-US', {
  sensitivity: 'base',
  usage: 'sort',
  ignorePunctuation: true,
  caseFirst: 'false',
});
// 自定义比较函数, 忽略空格并不区分大小写
function customCompare(a, b) {
  // 移除字符串中的空格并转为小写后再比较
  const stringA = a.replace(/\\s/g, '').toLowerCase();
  const stringB = b.replace(/\\s/g, '').toLowerCase();
  if (stringA < stringB) {
    return -1;
  }
  if (stringA > stringB) {
    return 1;
  }
  return 0;
}
const data = ['Apple', 'banana', 'cherry', 'apple pie', 'Banana Split', 'cherry tart'];
const data2 = data.slice()
// 老方式: 使用sort回调排序
console.log(data.sort(customCompare)); // 输出排序结果:['Apple', 'apple pie', 'banana', 'Banana Split', 'cherry', 'cherry tart']
// 新方式: 使用自定义Collator对象进行排序
console.log(data2.sort(customCollator.compare)); // 输出排序结果:['Apple', 'apple pie', 'banana', 'Banana Split', 'cherry', 'cherry tart']

可以发现两种方式结果一样, 但是明显Intl. Collator更加优雅, 是配置化的.

Intl.「ListFormat」

「Intl.ListFormat」 是 JavaScript 的国际化 API 之一,它用于格式化列表,以便在多语言环境中创建自然语言的列表表示。Intl.ListFormat 允许你指定列表项的连接方式(如逗号、"和" 等),以及列表项的样式和语言设置。

const listFormatter = new Intl.ListFormat('en-US', { style: 'long', type: 'disjunction' });
const items = ['apples', 'bananas', 'cherries'];
const formattedList = listFormatter.format(items);
console.log(formattedList); // 根据配置输出例如:"apples, bananas, or cherries"

const listFormatter = new Intl.ListFormat('en-US', { style: 'short', type: 'conjunction' });
const items = ['apples', 'bananas', 'cherries'];
const formattedList = listFormatter.format(items);
console.log(formattedList); // 根据配置输出例如:"apples, bananas, & cherries"

const listFormatter = new Intl.ListFormat('en-US', { style: 'narrow', type: 'conjunction' });
const items = ['apples', 'bananas', 'cherries'];
const formattedList = listFormatter.format(items);
console.log(formattedList); // 根据配置输出例如:"apples, bananas, cherries"

可以发现 options可以传递参数style和type, 有如下取值

  1. 「样式 (style)」: 「style」 选项指定列表的样式,有三个可能的值
  2. 'long':使用完整的语言表达,例如 "A, B, and C"。
  3. 'short':使用缩略形式,例如 "A, B, & C"。
  4. 'narrow':使用极简的形式,例如 "A B C"。
  5. 「连接方式 (type)」: 「type」 选项指定连接列表项的方式,有两个可能的值
  6. 'conjunction':使用 "和"(默认值),例如 "A、B和C"。
  7. 'disjunction':使用 "或",例如 "A、B或C"。

日历

日历是一种常见的东西, 在中国我们经常接触到公历和农历,公历全称格里高利历, 英文gregory。

现在国家节日很多都是跟随农历的,比如春节,中秋节等。以前家家人手一本农历, 上面会今日宜做什么, 现在很少见但是老人家还是信这个。

而与此相同, 每个地方都有自己的历法

  1. 「伊斯兰历(Hijri Calendar)」:也称为伊斯兰农历,是伊斯兰教的官方日历,基于月亮的循环。伊斯兰历的年份比公历年份短,每年有12个月,因此季节日期会变化。
  2. 「希伯来历(Hebrew Calendar)」:希伯来历是犹太教的官方日历,基于太阳和月亮的周期。它有13个月,其中一些月份可以有不同的天数,以保持与农历季节的一致性。
  3. 「农历(Lunar Calendar)」:农历基于月亮的循环,不同地区和文化有不同的农历系统,如中国农历、韩国农历、越南农历等。
  4. 「波斯历(Persian Calendar)」:波斯历,也称波斯太阳历,是伊朗和一些邻近国家使用的太阳历,与公历有一些差异。
  5. 「印度历法(Indian Calendar)」:印度有多种历法,包括维基历(Vikram Samvat)、国民历法(Saka Samvat)、泰米尔历法(Tamil Calendar)等。
  6. 「巴哈伊历(Bahá'í Calendar)」:巴哈伊信仰使用的独特日历,包括19个月,每个月19天。
  7. 「民族历法」:一些文化和民族拥有自己的独特历法,用于纪念特定历史事件和节日。
const date = new Date(); // 当前日期, Mon Oct 30 2023 20:00:50 GMT+0800 (中国标准时间)
const formattedDate = new Intl.DateTimeFormat('ar-SA-u-ca-islamic', { year: 'numeric', month: 'long', day: 'numeric' }).format(date);
console.log(formattedDate); // ١٦ ربيع الآخر ١٤٤٥ هـ

const date = new Date(1994, 1, 26);
const formattedDate = new Intl.DateTimeFormat('zh-CN-u-ca-chinese', { year: 'numeric', month: 'long', day: 'numeric' }).format(date);
console.log(formattedDate); // 1994甲戌年正月17

// 不同语言下不同日历的名称
const calendarNames = new Intl.DisplayNames('en-US', { type: 'calendar' });
console.log(calendarNames.of('gregory')); // 输出:Gregorian
console.log(calendarNames.of('islamic')); // 输出:Islamic

const calendarNames = new Intl.DisplayNames('zh-CN', { type: 'calendar' });
console.log(calendarNames.of('gregory')); // 输出:公历
console.log(calendarNames.of('islamic')); // 输出:伊斯兰历

除了上述gregory格里高利历, 取值还有下面这些

  1. "buddhist":佛教历(Buddhist Calendar)
  2. "chinese":中国农历(Chinese Lunar Calendar)
  3. "coptic":科普特历(Coptic Calendar)
  4. "ethiopic":埃塞俄比亚历(Ethiopic Calendar)
  5. "gregory":格里高利历(Gregorian Calendar,即公历)
  6. "hebrew":希伯来历(Hebrew Calendar)
  7. "indian":印度历法(Indian Calendar),包括维基历(Vikram Samvat)等
  8. "islamic":伊斯兰历(Islamic Calendar)
  9. "persian":波斯历(Persian Calendar)
  10. "islamic-civil":伊斯兰历的公民版本(Islamic Civil Calendar),通常用于文书、合同等民事事务

星期

不得不说日本的和历, 真的是很神奇. 不是中文的周一到周日, 也不是Sunday-Saturday. 首先日本还有皇帝, 有皇帝就有年号. 常见下面的一些年份

  1. 「明治时代 (Meiji Era)」:
  2. 年号:明治(Meiji)
  3. 年份范围:1868年 - 1912年
  4. 注释:明治时代标志着日本的近代化和工业化的开始。
  5. 「大正时代 (Taisho Era)」:
  6. 年号:大正(Taisho)
  7. 年份范围:1912年 - 1926年
  8. 注释:大正时代是日本的一个相对短暂时期,也标志着日本的一些政治和社会变革。
  9. 「昭和时代 (Showa Era)」:
  10. 年号:昭和(Showa)
  11. 年份范围:1926年 - 1989年
  12. 注释:昭和时代见证了日本的战争和战后重建,以及日本成为现代工业强国。
  13. 「平成时代 (Heisei Era)」:
  14. 年号:平成(Heisei)
  15. 年份范围:1989年 - 2019年
  16. 注释:平成时代包括了日本的经济繁荣和一些社会变革。
  17. 「令和时代 (Reiwa Era)」: 令和系奥特曼(https://baike.baidu.com/item/%E4%BB%A4%E5%92%8C%E7%B3%BB%E5%A5%A5%E7%89%B9%E6%9B%BC/50340521)
  18. 年号:令和(Reiwa)
  19. 年份范围:2019年 - 至今
  20. 注释:令和时代是日本当前的年号,始于2019年5月1日,标志着新的时代的开始。

可以看到日本的日历起始时周日(日), 但是周一到周六分别对应月火水木金土. 与众不同

// 获取今天过去7天的日期
// 日期对象
const today = new Date();

// 创建一个选项对象,指定输出的语言和风格
const optionsCN = { weekday: 'long' };
const optionsJP = { weekday: 'long' };

// 获取过去一周的日期
console.log('\n过去一周的日期:');
const CNArr = []
const JPArr = []
for (let i = 0; i < 7; i++) {
  const pastDate = new Date(today);
  pastDate.setDate(today.getDate() - i);
  CNArr.unshift(new Intl.DateTimeFormat('zh-CN', optionsCN).format(pastDate))
 JPArr.unshift(new Intl.DateTimeFormat('ja-JP', optionsJP).format(pastDate))
}
console.log('CNArr', CNArr.join(' ')) // CNArr 星期二 星期三 星期四 星期五 星期六 星期日 星期一
console.log('JPArr', JPArr.join(' ')) // JPArr 火曜日 水曜日 木曜日 金曜日 土曜日 日曜日 月曜日

日历单位

const dateTimeFields = new Intl.DisplayNames('en-US', { type: 'dateTimeField' });
console.log(dateTimeFields.of('era')); // 输出:Era, 纪元的意思
console.log(dateTimeFields.of('year')); // 输出:Year
console.log(dateTimeFields.of('month')); // 输出:Month
console.log(dateTimeFields.of('day')); // 输出:Day
console.log(dateTimeFields.of('weekday')); // 输出:Day of the week
console.log(dateTimeFields.of('hour')); // 输出:Hour
console.log(dateTimeFields.of('minute')); // 输出:Minute
console.log(dateTimeFields.of('second')); // 输出:Second
console.log(dateTimeFields.of('quarter')); // 输出:Quarter

const dateTimeFields = new Intl.DisplayNames('ja-JP', { type: 'dateTimeField' });
console.log(dateTimeFields.of('era')); // 输出:時代
console.log(dateTimeFields.of('year')); // 输出:年
console.log(dateTimeFields.of('month')); // 输出:月
console.log(dateTimeFields.of('day')); // 输出:日
console.log(dateTimeFields.of('weekday')); // 输出:曜日
console.log(dateTimeFields.of('hour')); // 输出:時
console.log(dateTimeFields.of('minute')); // 输出:分
console.log(dateTimeFields.of('second')); // 输出:秒
console.log(dateTimeFields.of('quarter')); // 输出:四半期

每周第一天

en-US一般每周第一天是周日, 而zh-CN一般每周第一天是周一. 可以通过如下信息判断

Intl. Locale函数返回属性里firstDay是一个数字,其中 0或7 表示星期日,1 表示星期一,依此类推。不同的地区和文化可能会将每周的第一天设置为不同的日期,因此这个属性可以帮助你确定每周的起始日期,例如,星期天或星期一。

(new Intl.Locale('zh-CN')).weekInfo // {"firstDay":1,"weekend":[6,7],"minimalDays":1}
(new Intl.Locale('en-US')).weekInfo // {"firstDay":7,"weekend":[6,7],"minimalDays":1}

const locale = 'zh-CN'
console.log(new Intl.DateTimeFormat(locale, { weekday: "long" }).format(new Date(0, 0, new Intl.Locale(locale).weekInfo.firstDay))) // '星期一'

const locale = 'en-US'
console.log(new Intl.DateTimeFormat(locale, { weekday: "long" }).format(new Date(0, 0, new Intl.Locale(locale).weekInfo.firstDay))) // 'Sunday'

能生效的原因就是 new Date(0,0,7) 等价于 new Date(1900,0,7) 对应1900.1.7(UTC+0), 此时对应us的sunday. 而 new Date(0,0,1) 对应1900.1.1(UTC+0), 对应cn的星期一

如何写一个国际化日历组件

  1. zh-CN下是星期一开头
  2. en-US是Sunday开头
  3. 日语环境符合 火曜日 水曜日 木曜日 金曜日 土曜日 日曜日 月曜日等描述

Intl-region-language-calendar:https://codesandbox.io/p/devbox/intl-region-language-calendar-xm7ls8?embed=1&file=%2Fsrc%2FApp.tsx

实现细节

  1. 如何获取一周的第一天
const localeUS = 'en-US'
new Intl.DateTimeFormat(localeUS, { weekday: "long" }).format(new Date(0, 0, new Intl.Locale(localeUS).weekInfo.firstDay)) // 'Sunday'

const localeCN = 'zh-CN'
new Intl.DateTimeFormat(localeCN, { weekday: "long" }).format(new Date(0, 0, new Intl.Locale(localeCN).weekInfo.firstDay)) // '星期一'
  1. 输出一周7天
const locale = "en-US";
const firstDay = new Intl.Locale(locale).weekInfo.firstDay;
const formatInstace = new Intl.DateTimeFormat(locale, { weekday: "long" });
for (let i = 0; i < 7; i++) {
  console.log(formatInstace.format(new Date(0, 0, firstDay + i)));
}
// Sunday
// Monday
// Tuesday
// Wednesday
// Thursday
// Friday
// Saturday

const localeCN = "zh-CN";
const firstDayCN = new Intl.Locale(localeCN).weekInfo.firstDay;
const formatInstaceCN = new Intl.DateTimeFormat(localeCN, { weekday: "long" });
for (let i = 0; i < 7; i++) {
  console.log(formatInstaceCN.format(new Date(0, 0, firstDayCN + i)));
}
// 星期一
// 星期二
// 星期三
// 星期四
// 星期五
// 星期六
// 星期日

Intl支持

支持查询

可以通过API, Intl.supportedValuesOf 获取到所有支持的

const calendars = Intl.supportedValuesOf("calendar");
console.log(calendars); // 输出所有支持的日历系统
// (18) ['buddhist', 'chinese', 'coptic', 'dangi', 'ethioaa', 'ethiopic', 'gregory', 'hebrew', 'indian', 'islamic', 'islamic-civil', 'islamic-rgsa', 'islamic-tbla', 'islamic-umalqura', 'iso8601', 'japanese', 'persian', 'roc']

const currencies = Intl.supportedValuesOf("currency");
console.log(currencies); // 输出所有支持的货币代码
// (159) ['AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN', 'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL', 'BSD', 'BTN', 'BWP', 'BYN', 'BZD', 'CAD', 'CDF', 'CHF', 'CLP', 'CNY', 'COP', 'CRC', 'CUC', 'CUP', 'CVE', 'CZK', 'DJF', 'DKK', 'DOP', 'DZD', 'EGP', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GHS', 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF', 'IDR', 'ILS', 'INR', 'IQD', 'IRR', 'ISK', 'JMD', 'JOD', 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW', 'KWD', 'KYD', 'KZT', 'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LYD', 'MAD', 'MDL', 'MGA', 'MKD', 'MMK', 'MNT', 'MOP', 'MRU', 'MUR', 'MVR', 'MWK', 'MXN', 'MYR', 'MZN', 'NAD', 'NGN', 'NIO', …]

const timeZones = Intl.supportedValuesOf("timeZone");
console.log(timeZones); // 输出所有支持的时区
// (428) ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmera', 'Africa/Bamako', 'Africa/Bangui', 'Africa/Banjul', 'Africa/Bissau', 'Africa/Blantyre', 'Africa/Brazzaville', 'Africa/Bujumbura', 'Africa/Cairo', 'Africa/Casablanca', 'Africa/Ceuta', 'Africa/Conakry', 'Africa/Dakar', 'Africa/Dar_es_Salaam', 'Africa/Djibouti', 'Africa/Douala', 'Africa/El_Aaiun', 'Africa/Freetown', 'Africa/Gaborone', 'Africa/Harare', 'Africa/Johannesburg', 'Africa/Juba', 'Africa/Kampala', 'Africa/Khartoum', 'Africa/Kigali', 'Africa/Kinshasa', 'Africa/Lagos', 'Africa/Libreville', 'Africa/Lome', 'Africa/Luanda', 'Africa/Lubumbashi', 'Africa/Lusaka', 'Africa/Malabo', 'Africa/Maputo', 'Africa/Maseru', 'Africa/Mbabane', 'Africa/Mogadishu', 'Africa/Monrovia', 'Africa/Nairobi', 'Africa/Ndjamena', 'Africa/Niamey', 'Africa/Nouakchott', 'Africa/Ouagadougou', 'Africa/Porto-Novo', 'Africa/Sao_Tome', 'Africa/Tripoli', 'Africa/Tunis', 'Africa/Windhoek', 'America/Adak', 'America/Anchorage', 'America/Anguilla', 'America/Antigua', 'America/Araguaina', 'America/Argentina/La_Rioja', 'America/Argentina/Rio_Gallegos', 'America/Argentina/Salta', 'America/Argentina/San_Juan', 'America/Argentina/San_Luis', 'America/Argentina/Tucuman', 'America/Argentina/Ushuaia', 'America/Aruba', 'America/Asuncion', 'America/Bahia', 'America/Bahia_Banderas', 'America/Barbados', 'America/Belem', 'America/Belize', 'America/Blanc-Sablon', 'America/Boa_Vista', 'America/Bogota', 'America/Boise', 'America/Buenos_Aires', 'America/Cambridge_Bay', 'America/Campo_Grande', 'America/Cancun', 'America/Caracas', 'America/Catamarca', 'America/Cayenne', 'America/Cayman', 'America/Chicago', 'America/Chihuahua', 'America/Ciudad_Juarez', 'America/Coral_Harbour', 'America/Cordoba', 'America/Costa_Rica', 'America/Creston', 'America/Cuiaba', 'America/Curacao', 'America/Danmarkshavn', 'America/Dawson', 'America/Dawson_Creek', 'America/Denver', 'America/Detroit', 'America/Dominica', 'America/Edmonton', 'America/Eirunepe', …]

低版本兼容

Intl是浏览器对i18n提供的底层API, 用于处理国际化相关内容. 附带browerstack 云真机测试工具(caniuse推荐): https://live.browserstack.com/dashboard如果没处理好兼容性问题直接使用API, 会报JS Error. 内容为Intl.DisplayNames is not a constructor

对应的, 一些操作系统低版本的用户(长期不升级系统)会遇到JS Error导致白屏

Google pixel4(2019, October 15发行)

formatjs

可以用formatjs提供的polyfill做低版本兼容: https://formatjs.io/docs/getting-started/installation

  1. 支持关系有先后依赖, 需要按照依赖顺序依次倒入对应的包
  2. 移动端场景如小程序, 如果全量导入移动端场景会让包体积爆炸(普通压缩包Gzip后2.6M, 全量导入后会到9.1M). 最后polyfill按照顺序依次导入, 且移动端场景只兜底英文部分, 限制包体积在3M内
async function loadPolyfill() {
    // 如果当前环境不支持 Intl 或者 Intl.DisplayNames
    if (!window.Intl || !window.Intl.DisplayNames) {
        window.Intl = window.Intl || {}
        // 加载 polyfill
        await import('@formatjs/intl-getcanonicallocales/polyfill-force')
        await import('@formatjs/intl-locale/polyfill-force')
        await import('@formatjs/intl-displaynames/polyfill-force')
        await import('@formatjs/intl-displaynames/locale-data/en')
        return false
    } else {
        // 当前环境支持 Intl.DisplayNames API,不需要 Polyfill
        return true
    }
}

Why not Babel?

众所周知Babel有一个babel-preset-env(https://www.babeljs.cn/docs/babel-preset-env#how-does-it-work), 用于在编译代码时智能(基于core-js-compat(https://www.npmjs.com/package/core-js-compat))引入helper和polyfill 智能的含义: 可以设置最低兼容的浏览器(https://github.com/browserslist/browserslist#queries)和代码, 动态引用所需的helper和polyfill

// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage', // 根据每个文件里面,用到了哪些es的新特性和targets导入polyfill,更加精简
        corejs: 3, // 指定 core-js 版本
        targets: "> 0.25%, not dead" // 指定目标浏览器, 选取全球使用率超过 0.25% 的浏览器版本
      },
    ],
  ],
};

「babel底层使用core-js(https://github.com/zloirock/core-js)进行polyfill, 但是core-js不包含Intl API部分的polyfill(https://github.com/zloirock/core-js?tab=readme-ov-file#missing-polyfills), 所以babel并不能为Intl API做polyfill」

Nodejs使用

  1. 安装
npm i @formatjs/intl
  1. 使用
import {createIntl, createIntlCache} from '@formatjs/intl'

// This is optional but highly recommended
// since it prevents memory leak
const cache = createIntlCache()

const intlFr = createIntl(
  {
    locale: 'fr-FR',
    messages: {},
  },
  cache
)
const intlEn = createIntl(
  {
    locale: 'en-US',
    message: {},
    cache
  }
)

// Call imperatively
console.log(intlFr.formatNumber(2000000000000)) // 2 000 000 000 000
console.log(intlEn.formatNumber(2000000000000)) // 2,000,000,000,000

作者:ManfredHu

来源-微信公众号:字节前端 ByteFE

出处:https://mp.weixin.qq.com/s/PByp6Pmc3vp7b0acyPT8yA

萄牙房地产专业人士和中介协会近日指出,里斯本和波尔图房地产市场应增加新建筑以实现价格平衡。

据悉,葡萄牙房地产专业人士和中介协会主席路易斯·利马表示:“葡萄牙还不会出现房地产泡沫,因为与欧洲普遍房价相比,我们还有很大的距离。现在在里斯本和波尔图地区出现一些小的泡沫令人担忧,因为涨幅超过了合理的规律。无论是国家中央还是地方当局都可以适当地阻止这种价格上涨,例如在里斯本和波尔图地区应该新增一些房地产建筑,以合理的价格出售。”

目前,房地产市场与旅游业的增长紧密联系在一起,从某种意义上来说,曾经在阿尔加维地区发生的旅游住宅短租现象现在在里斯本和波尔图也出现了火热的效应,旅游住宅的压力影响到了普通住宅领域。其中,里斯本和波尔图市中心的新建筑空缺导致城市外围房价也开始增涨。

根据路易斯·利马的说法,葡萄牙今年的房地产市场预计将增长30%左右,主要得益于外国投资的增加和国民消费信心的增长。“房地产业和旅游业正在成为国民经济的引擎,但是现在出现供不应求的现象,国家应该提高房产的供应。仅仅靠旧房修复是满足不了日益增长的需求,有必要在里斯本和波尔图投资建造新的建筑。”

路易斯·利马指出:“甚至没有必要到郊外去,因为即使是在里斯本中心也仍有许多空地可以新建住房。”

许多人对目前里斯本和波尔图的“大幅涨价”而感到担忧,甚至很多葡萄牙家庭都表示这个价格“无法忍受”,路易斯·利马对此回应道:“目前葡萄牙的房地产价格上涨了7%,仍然是在合理的范围内。我们比欧洲其他国家的价格要低很多,例如我们是马德里价格的一半,巴黎价格的四分之一,伦敦的八分之一。不过葡萄牙人的收入也与西班牙、法国和英国不同。”

葡华报编译

原文链接:https://www.dn.pt/portugal/interior/mediadores-imobiliarios-consideram-urgente-aumento-da-construcao-nova-em-lisboa-e-porto--9501959.html