uni-i18n.es.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. const isObject = (val) => val !== null && typeof val === 'object';
  2. const defaultDelimiters = ['{', '}'];
  3. class BaseFormatter {
  4. constructor() {
  5. this._caches = Object.create(null);
  6. }
  7. interpolate(message, values, delimiters = defaultDelimiters) {
  8. if (!values) {
  9. return [message];
  10. }
  11. let tokens = this._caches[message];
  12. if (!tokens) {
  13. tokens = parse(message, delimiters);
  14. this._caches[message] = tokens;
  15. }
  16. return compile(tokens, values);
  17. }
  18. }
  19. const RE_TOKEN_LIST_VALUE = /^(?:\d)+/;
  20. const RE_TOKEN_NAMED_VALUE = /^(?:\w)+/;
  21. function parse(format, [startDelimiter, endDelimiter]) {
  22. const tokens = [];
  23. let position = 0;
  24. let text = '';
  25. while (position < format.length) {
  26. let char = format[position++];
  27. if (char === startDelimiter) {
  28. if (text) {
  29. tokens.push({ type: 'text', value: text });
  30. }
  31. text = '';
  32. let sub = '';
  33. char = format[position++];
  34. while (char !== undefined && char !== endDelimiter) {
  35. sub += char;
  36. char = format[position++];
  37. }
  38. const isClosed = char === endDelimiter;
  39. const type = RE_TOKEN_LIST_VALUE.test(sub)
  40. ? 'list'
  41. : isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
  42. ? 'named'
  43. : 'unknown';
  44. tokens.push({ value: sub, type });
  45. }
  46. // else if (char === '%') {
  47. // // when found rails i18n syntax, skip text capture
  48. // if (format[position] !== '{') {
  49. // text += char
  50. // }
  51. // }
  52. else {
  53. text += char;
  54. }
  55. }
  56. text && tokens.push({ type: 'text', value: text });
  57. return tokens;
  58. }
  59. function compile(tokens, values) {
  60. const compiled = [];
  61. let index = 0;
  62. const mode = Array.isArray(values)
  63. ? 'list'
  64. : isObject(values)
  65. ? 'named'
  66. : 'unknown';
  67. if (mode === 'unknown') {
  68. return compiled;
  69. }
  70. while (index < tokens.length) {
  71. const token = tokens[index];
  72. switch (token.type) {
  73. case 'text':
  74. compiled.push(token.value);
  75. break;
  76. case 'list':
  77. compiled.push(values[parseInt(token.value, 10)]);
  78. break;
  79. case 'named':
  80. if (mode === 'named') {
  81. compiled.push(values[token.value]);
  82. }
  83. else {
  84. if (process.env.NODE_ENV !== 'production') {
  85. console.warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`);
  86. }
  87. }
  88. break;
  89. case 'unknown':
  90. if (process.env.NODE_ENV !== 'production') {
  91. console.warn(`Detect 'unknown' type of token!`);
  92. }
  93. break;
  94. }
  95. index++;
  96. }
  97. return compiled;
  98. }
  99. const LOCALE_ZH_HANS = 'zh-Hans';
  100. const LOCALE_ZH_HANT = 'zh-Hant';
  101. const LOCALE_EN = 'en';
  102. const LOCALE_FR = 'fr';
  103. const LOCALE_ES = 'es';
  104. const hasOwnProperty = Object.prototype.hasOwnProperty;
  105. const hasOwn = (val, key) => hasOwnProperty.call(val, key);
  106. const defaultFormatter = new BaseFormatter();
  107. function include(str, parts) {
  108. return !!parts.find((part) => str.indexOf(part) !== -1);
  109. }
  110. function startsWith(str, parts) {
  111. return parts.find((part) => str.indexOf(part) === 0);
  112. }
  113. function normalizeLocale(locale, messages) {
  114. if (!locale) {
  115. return;
  116. }
  117. locale = locale.trim().replace(/_/g, '-');
  118. if (messages && messages[locale]) {
  119. return locale;
  120. }
  121. locale = locale.toLowerCase();
  122. if (locale === 'chinese') {
  123. // 支付宝
  124. return LOCALE_ZH_HANS;
  125. }
  126. if (locale.indexOf('zh') === 0) {
  127. if (locale.indexOf('-hans') > -1) {
  128. return LOCALE_ZH_HANS;
  129. }
  130. if (locale.indexOf('-hant') > -1) {
  131. return LOCALE_ZH_HANT;
  132. }
  133. if (include(locale, ['-tw', '-hk', '-mo', '-cht'])) {
  134. return LOCALE_ZH_HANT;
  135. }
  136. return LOCALE_ZH_HANS;
  137. }
  138. let locales = [LOCALE_EN, LOCALE_FR, LOCALE_ES];
  139. if (messages && Object.keys(messages).length > 0) {
  140. locales = Object.keys(messages);
  141. }
  142. const lang = startsWith(locale, locales);
  143. if (lang) {
  144. return lang;
  145. }
  146. }
  147. class I18n {
  148. constructor({ locale, fallbackLocale, messages, watcher, formater, }) {
  149. this.locale = LOCALE_EN;
  150. this.fallbackLocale = LOCALE_EN;
  151. this.message = {};
  152. this.messages = {};
  153. this.watchers = [];
  154. if (fallbackLocale) {
  155. this.fallbackLocale = fallbackLocale;
  156. }
  157. this.formater = formater || defaultFormatter;
  158. this.messages = messages || {};
  159. this.setLocale(locale || LOCALE_EN);
  160. if (watcher) {
  161. this.watchLocale(watcher);
  162. }
  163. }
  164. setLocale(locale) {
  165. const oldLocale = this.locale;
  166. this.locale = normalizeLocale(locale, this.messages) || this.fallbackLocale;
  167. if (!this.messages[this.locale]) {
  168. // 可能初始化时不存在
  169. this.messages[this.locale] = {};
  170. }
  171. this.message = this.messages[this.locale];
  172. // 仅发生变化时,通知
  173. if (oldLocale !== this.locale) {
  174. this.watchers.forEach((watcher) => {
  175. watcher(this.locale, oldLocale);
  176. });
  177. }
  178. }
  179. getLocale() {
  180. return this.locale;
  181. }
  182. watchLocale(fn) {
  183. const index = this.watchers.push(fn) - 1;
  184. return () => {
  185. this.watchers.splice(index, 1);
  186. };
  187. }
  188. add(locale, message, override = true) {
  189. const curMessages = this.messages[locale];
  190. if (curMessages) {
  191. if (override) {
  192. Object.assign(curMessages, message);
  193. }
  194. else {
  195. Object.keys(message).forEach((key) => {
  196. if (!hasOwn(curMessages, key)) {
  197. curMessages[key] = message[key];
  198. }
  199. });
  200. }
  201. }
  202. else {
  203. this.messages[locale] = message;
  204. }
  205. }
  206. f(message, values, delimiters) {
  207. return this.formater.interpolate(message, values, delimiters).join('');
  208. }
  209. t(key, locale, values) {
  210. let message = this.message;
  211. if (typeof locale === 'string') {
  212. locale = normalizeLocale(locale, this.messages);
  213. locale && (message = this.messages[locale]);
  214. }
  215. else {
  216. values = locale;
  217. }
  218. if (!hasOwn(message, key)) {
  219. console.warn(`Cannot translate the value of keypath ${key}. Use the value of keypath as default.`);
  220. return key;
  221. }
  222. return this.formater.interpolate(message[key], values).join('');
  223. }
  224. }
  225. function watchAppLocale(appVm, i18n) {
  226. // 需要保证 watch 的触发在组件渲染之前
  227. if (appVm.$watchLocale) {
  228. // vue2
  229. appVm.$watchLocale((newLocale) => {
  230. i18n.setLocale(newLocale);
  231. });
  232. }
  233. else {
  234. appVm.$watch(() => appVm.$locale, (newLocale) => {
  235. i18n.setLocale(newLocale);
  236. });
  237. }
  238. }
  239. function getDefaultLocale() {
  240. if (typeof uni !== 'undefined' && uni.getLocale) {
  241. return uni.getLocale();
  242. }
  243. // 小程序平台,uni 和 uni-i18n 互相引用,导致访问不到 uni,故在 global 上挂了 getLocale
  244. if (typeof global !== 'undefined' && global.getLocale) {
  245. return global.getLocale();
  246. }
  247. return LOCALE_EN;
  248. }
  249. function initVueI18n(locale, messages = {}, fallbackLocale, watcher) {
  250. // 兼容旧版本入参
  251. if (typeof locale !== 'string') {
  252. [locale, messages] = [
  253. messages,
  254. locale,
  255. ];
  256. }
  257. if (typeof locale !== 'string') {
  258. // 因为小程序平台,uni-i18n 和 uni 互相引用,导致此时访问 uni 时,为 undefined
  259. locale = getDefaultLocale();
  260. }
  261. if (typeof fallbackLocale !== 'string') {
  262. fallbackLocale =
  263. (typeof __uniConfig !== 'undefined' && __uniConfig.fallbackLocale) ||
  264. LOCALE_EN;
  265. }
  266. const i18n = new I18n({
  267. locale,
  268. fallbackLocale,
  269. messages,
  270. watcher,
  271. });
  272. let t = (key, values) => {
  273. if (typeof getApp !== 'function') {
  274. // app view
  275. /* eslint-disable no-func-assign */
  276. t = function (key, values) {
  277. return i18n.t(key, values);
  278. };
  279. }
  280. else {
  281. let isWatchedAppLocale = false;
  282. t = function (key, values) {
  283. const appVm = getApp().$vm;
  284. // 可能$vm还不存在,比如在支付宝小程序中,组件定义较早,在props的default里使用了t()函数(如uni-goods-nav),此时app还未初始化
  285. // options: {
  286. // type: Array,
  287. // default () {
  288. // return [{
  289. // icon: 'shop',
  290. // text: t("uni-goods-nav.options.shop"),
  291. // }, {
  292. // icon: 'cart',
  293. // text: t("uni-goods-nav.options.cart")
  294. // }]
  295. // }
  296. // },
  297. if (appVm) {
  298. // 触发响应式
  299. appVm.$locale;
  300. if (!isWatchedAppLocale) {
  301. isWatchedAppLocale = true;
  302. watchAppLocale(appVm, i18n);
  303. }
  304. }
  305. return i18n.t(key, values);
  306. };
  307. }
  308. return t(key, values);
  309. };
  310. return {
  311. i18n,
  312. f(message, values, delimiters) {
  313. return i18n.f(message, values, delimiters);
  314. },
  315. t(key, values) {
  316. return t(key, values);
  317. },
  318. add(locale, message, override = true) {
  319. return i18n.add(locale, message, override);
  320. },
  321. watch(fn) {
  322. return i18n.watchLocale(fn);
  323. },
  324. getLocale() {
  325. return i18n.getLocale();
  326. },
  327. setLocale(newLocale) {
  328. return i18n.setLocale(newLocale);
  329. },
  330. };
  331. }
  332. const isString = (val) => typeof val === 'string';
  333. let formater;
  334. function hasI18nJson(jsonObj, delimiters) {
  335. if (!formater) {
  336. formater = new BaseFormatter();
  337. }
  338. return walkJsonObj(jsonObj, (jsonObj, key) => {
  339. const value = jsonObj[key];
  340. if (isString(value)) {
  341. if (isI18nStr(value, delimiters)) {
  342. return true;
  343. }
  344. }
  345. else {
  346. return hasI18nJson(value, delimiters);
  347. }
  348. });
  349. }
  350. function parseI18nJson(jsonObj, values, delimiters) {
  351. if (!formater) {
  352. formater = new BaseFormatter();
  353. }
  354. walkJsonObj(jsonObj, (jsonObj, key) => {
  355. const value = jsonObj[key];
  356. if (isString(value)) {
  357. if (isI18nStr(value, delimiters)) {
  358. jsonObj[key] = compileStr(value, values, delimiters);
  359. }
  360. }
  361. else {
  362. parseI18nJson(value, values, delimiters);
  363. }
  364. });
  365. return jsonObj;
  366. }
  367. function compileI18nJsonStr(jsonStr, { locale, locales, delimiters, }) {
  368. if (!isI18nStr(jsonStr, delimiters)) {
  369. return jsonStr;
  370. }
  371. if (!formater) {
  372. formater = new BaseFormatter();
  373. }
  374. const localeValues = [];
  375. Object.keys(locales).forEach((name) => {
  376. if (name !== locale) {
  377. localeValues.push({
  378. locale: name,
  379. values: locales[name],
  380. });
  381. }
  382. });
  383. localeValues.unshift({ locale, values: locales[locale] });
  384. try {
  385. return JSON.stringify(compileJsonObj(JSON.parse(jsonStr), localeValues, delimiters), null, 2);
  386. }
  387. catch (e) { }
  388. return jsonStr;
  389. }
  390. function isI18nStr(value, delimiters) {
  391. return value.indexOf(delimiters[0]) > -1;
  392. }
  393. function compileStr(value, values, delimiters) {
  394. return formater.interpolate(value, values, delimiters).join('');
  395. }
  396. function compileValue(jsonObj, key, localeValues, delimiters) {
  397. const value = jsonObj[key];
  398. if (isString(value)) {
  399. // 存在国际化
  400. if (isI18nStr(value, delimiters)) {
  401. jsonObj[key] = compileStr(value, localeValues[0].values, delimiters);
  402. if (localeValues.length > 1) {
  403. // 格式化国际化语言
  404. const valueLocales = (jsonObj[key + 'Locales'] = {});
  405. localeValues.forEach((localValue) => {
  406. valueLocales[localValue.locale] = compileStr(value, localValue.values, delimiters);
  407. });
  408. }
  409. }
  410. }
  411. else {
  412. compileJsonObj(value, localeValues, delimiters);
  413. }
  414. }
  415. function compileJsonObj(jsonObj, localeValues, delimiters) {
  416. walkJsonObj(jsonObj, (jsonObj, key) => {
  417. compileValue(jsonObj, key, localeValues, delimiters);
  418. });
  419. return jsonObj;
  420. }
  421. function walkJsonObj(jsonObj, walk) {
  422. if (Array.isArray(jsonObj)) {
  423. for (let i = 0; i < jsonObj.length; i++) {
  424. if (walk(jsonObj, i)) {
  425. return true;
  426. }
  427. }
  428. }
  429. else if (isObject(jsonObj)) {
  430. for (const key in jsonObj) {
  431. if (walk(jsonObj, key)) {
  432. return true;
  433. }
  434. }
  435. }
  436. return false;
  437. }
  438. function resolveLocale(locales) {
  439. return (locale) => {
  440. if (!locale) {
  441. return locale;
  442. }
  443. locale = normalizeLocale(locale) || locale;
  444. return resolveLocaleChain(locale).find((locale) => locales.indexOf(locale) > -1);
  445. };
  446. }
  447. function resolveLocaleChain(locale) {
  448. const chain = [];
  449. const tokens = locale.split('-');
  450. while (tokens.length) {
  451. chain.push(tokens.join('-'));
  452. tokens.pop();
  453. }
  454. return chain;
  455. }
  456. export { BaseFormatter as Formatter, I18n, LOCALE_EN, LOCALE_ES, LOCALE_FR, LOCALE_ZH_HANS, LOCALE_ZH_HANT, compileI18nJsonStr, hasI18nJson, initVueI18n, isI18nStr, isString, normalizeLocale, parseI18nJson, resolveLocale };