From eb70ee06a70af6f0853c844c602b000f78046ab2 Mon Sep 17 00:00:00 2001 From: fxy060608 Date: Wed, 3 Mar 2021 19:52:11 +0800 Subject: [PATCH] feat: add uni-i18n --- packages/uni-h5/dist/uni-h5.esm.js | 54 ++-- packages/uni-h5/src/framework/app.ts | 2 +- .../src/framework/components/app/index.vue | 23 +- .../uni-h5/src/framework/plugin/router.ts | 37 ++- packages/uni-i18n/LICENSE | 202 ++++++++++++ packages/uni-i18n/__tests__/i18n.spec.ts | 116 +++++++ packages/uni-i18n/api-extractor.json | 8 + packages/uni-i18n/build.json | 8 + packages/uni-i18n/dist/uni-i18n.cjs.js | 287 ++++++++++++++++++ packages/uni-i18n/dist/uni-i18n.d.ts | 50 +++ packages/uni-i18n/dist/uni-i18n.esm.js | 282 +++++++++++++++++ packages/uni-i18n/package.json | 21 ++ packages/uni-i18n/src/I18n.ts | 150 +++++++++ packages/uni-i18n/src/format.ts | 123 ++++++++ packages/uni-i18n/src/index.ts | 2 + packages/uni-i18n/src/vue-i18n.ts | 103 +++++++ packages/uni-i18n/tsconfig.json | 18 ++ rollup.config.js | 2 + scripts/build.js | 5 +- 19 files changed, 1433 insertions(+), 60 deletions(-) create mode 100755 packages/uni-i18n/LICENSE create mode 100644 packages/uni-i18n/__tests__/i18n.spec.ts create mode 100644 packages/uni-i18n/api-extractor.json create mode 100644 packages/uni-i18n/build.json create mode 100644 packages/uni-i18n/dist/uni-i18n.cjs.js create mode 100644 packages/uni-i18n/dist/uni-i18n.d.ts create mode 100644 packages/uni-i18n/dist/uni-i18n.esm.js create mode 100644 packages/uni-i18n/package.json create mode 100644 packages/uni-i18n/src/I18n.ts create mode 100644 packages/uni-i18n/src/format.ts create mode 100644 packages/uni-i18n/src/index.ts create mode 100644 packages/uni-i18n/src/vue-i18n.ts create mode 100644 packages/uni-i18n/tsconfig.json diff --git a/packages/uni-h5/dist/uni-h5.esm.js b/packages/uni-h5/dist/uni-h5.esm.js index 4ba1b1407..66a20ffa3 100644 --- a/packages/uni-h5/dist/uni-h5.esm.js +++ b/packages/uni-h5/dist/uni-h5.esm.js @@ -690,7 +690,7 @@ function getApp$1() { function getCurrentPages$1() { return []; } -let id = 0; +let id = 1; function createPageState(type) { return { __id__: id++, @@ -712,9 +712,8 @@ const scrollBehavior = (to, from, savedPosition) => { } }; function createRouterOptions() { - const history2 = __UNI_ROUTER_MODE__ === "history" ? createWebHistory() : createWebHashHistory(); return { - history: history2, + history: initHistory(), strict: !!__uniConfig.router.strict, routes: __uniRoutes, scrollBehavior @@ -728,12 +727,25 @@ function createAppRouter(router) { initGuard(router); return router; } +function initHistory() { + const history2 = __UNI_ROUTER_MODE__ === "history" ? createWebHistory() : createWebHashHistory(); + history2.listen((_to, from, info) => { + if (info.direction === "back") { + const app = getApp$1(); + const id2 = history2.state.__id__; + if (app && id2) { + app.$refs.app.keepAliveExclude = [from + "-" + id2]; + } + } + }); + return history2; +} const beforeEach = (to, from, next) => { - const app = getApp$1(); - if (app) - next(); + next(); }; const afterEach = (to, from, failure) => { + console.log("afterEach.id", history.state.__id__); + console.log("afterEach", to, from, failure, JSON.stringify(history.state)); }; var tabBar_vue_vue_type_style_index_0_lang = "\nuni-tabbar {\r\n display: block;\r\n box-sizing: border-box;\r\n position: fixed;\r\n left: 0;\r\n bottom: 0;\r\n width: 100%;\r\n z-index: 998;\n}\nuni-tabbar .uni-tabbar {\r\n display: flex;\r\n position: fixed;\r\n left: 0;\r\n bottom: 0;\r\n width: 100%;\r\n z-index: 998;\r\n box-sizing: border-box;\r\n padding-bottom: 0;\r\n padding-bottom: constant(safe-area-inset-bottom);\r\n padding-bottom: env(safe-area-inset-bottom);\n}\nuni-tabbar .uni-tabbar ~ .uni-placeholder {\r\n width: 100%;\r\n height: 50px;\r\n margin-bottom: 0;\r\n margin-bottom: constant(safe-area-inset-bottom);\r\n margin-bottom: env(safe-area-inset-bottom);\n}\nuni-tabbar .uni-tabbar * {\r\n box-sizing: border-box;\n}\nuni-tabbar .uni-tabbar__item {\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n flex-direction: column;\r\n flex: 1;\r\n font-size: 0;\r\n text-align: center;\r\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nuni-tabbar .uni-tabbar__bd {\r\n position: relative;\r\n height: 50px;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\n}\nuni-tabbar .uni-tabbar__icon {\r\n position: relative;\r\n display: inline-block;\r\n margin-top: 5px;\r\n width: 24px;\r\n height: 24px;\n}\nuni-tabbar .uni-tabbar__icon.uni-tabbar__icon__diff {\r\n margin-top: 0px;\r\n width: 34px;\r\n height: 34px;\n}\nuni-tabbar .uni-tabbar__icon img {\r\n width: 100%;\r\n height: 100%;\n}\nuni-tabbar .uni-tabbar__label {\r\n position: relative;\r\n text-align: center;\r\n font-size: 10px;\r\n line-height: 1.8;\n}\nuni-tabbar .uni-tabbar-border {\r\n position: absolute;\r\n left: 0;\r\n top: 0;\r\n width: 100%;\r\n height: 1px;\r\n transform: scaleY(0.5);\n}\nuni-tabbar .uni-tabbar__reddot {\r\n position: absolute;\r\n top: 0;\r\n right: 0;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n background-color: #f43530;\r\n color: #ffffff;\r\n transform: translate(40%, -20%);\n}\nuni-tabbar .uni-tabbar__badge {\r\n width: auto;\r\n height: 16px;\r\n line-height: 16px;\r\n border-radius: 16px;\r\n min-width: 16px;\r\n padding: 0 2px;\r\n font-size: 12px;\r\n text-align: center;\r\n white-space: nowrap;\n}\r\n"; const _sfc_main = { @@ -2034,31 +2046,18 @@ const _sfc_main$4 = { name: "App", components, mixins, - props: { - keepAliveInclude: { - type: Array, - default: function() { - return []; - } - }, - keepAliveExclude: { - type: Array, - default: function() { - return []; - } - } - }, data() { return { transitionName: "fade", hideTabBar: false, tabBar: __uniConfig.tabBar || {}, - sysComponents: this.$sysComponents + sysComponents: this.$sysComponents, + keepAliveExclude: [] }; }, computed: { key() { - return this.$route.path + "-" + (history.state.__id__ || -1); + return this.$route.path + "-" + (history.state.__id__ || 0); }, hasTabBar() { return __uniConfig.tabBar && __uniConfig.tabBar.list && __uniConfig.tabBar.list.length; @@ -2111,14 +2110,11 @@ function _sfc_render$4(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createBlock("uni-app", { class: {"uni-app--showtabbar": $options.showTabBar} }, [ - createVNode(_component_router_view, {key: $options.key}, { + createVNode(_component_router_view, null, { default: withCtx(({Component}) => [ - (openBlock(), createBlock(KeepAlive, { - include: $props.keepAliveInclude, - exclude: $props.keepAliveExclude - }, [ - (openBlock(), createBlock(resolveDynamicComponent(Component))) - ], 1032, ["include", "exclude"])) + (openBlock(), createBlock(KeepAlive, {exclude: $data.keepAliveExclude}, [ + (openBlock(), createBlock(resolveDynamicComponent(Component), {key: $options.key})) + ], 1032, ["exclude"])) ]), _: 1 }), diff --git a/packages/uni-h5/src/framework/app.ts b/packages/uni-h5/src/framework/app.ts index 47345fc7d..8ec0605a4 100644 --- a/packages/uni-h5/src/framework/app.ts +++ b/packages/uni-h5/src/framework/app.ts @@ -10,7 +10,7 @@ export function getCurrentPages() { return [] } -let id = 0 +let id = 1 export function createPageState( type: 'navigateTo' | 'redirectTo' | 'reLaunch' | 'switchTab' ) { diff --git a/packages/uni-h5/src/framework/components/app/index.vue b/packages/uni-h5/src/framework/components/app/index.vue index 6fa8190f5..0cf4e0490 100644 --- a/packages/uni-h5/src/framework/components/app/index.vue +++ b/packages/uni-h5/src/framework/components/app/index.vue @@ -2,9 +2,9 @@ - - - + + + @@ -44,31 +44,18 @@ export default { name: 'App', components, mixins, - props: { - keepAliveInclude: { - type: Array, - default: function () { - return [] - }, - }, - keepAliveExclude: { - type: Array, - default: function () { - return [] - }, - }, - }, data() { return { transitionName: 'fade', hideTabBar: false, tabBar: __uniConfig.tabBar || {}, sysComponents: this.$sysComponents, + keepAliveExclude: [], } }, computed: { key() { - return this.$route.path + '-' + (history.state.__id__ || -1) + return this.$route.path + '-' + (history.state.__id__ || 0) }, hasTabBar() { return ( diff --git a/packages/uni-h5/src/framework/plugin/router.ts b/packages/uni-h5/src/framework/plugin/router.ts index 7c066bf5e..6ca9cf1b2 100644 --- a/packages/uni-h5/src/framework/plugin/router.ts +++ b/packages/uni-h5/src/framework/plugin/router.ts @@ -1,10 +1,10 @@ import { App } from 'vue' import { - NavigationGuardWithThis, - NavigationHookAfter, Router, - RouteRecordRaw, RouterOptions, + RouteRecordRaw, + NavigationHookAfter, + NavigationGuardWithThis, } from 'vue-router' import { createRouter, @@ -29,12 +29,8 @@ const scrollBehavior: RouterOptions['scrollBehavior'] = ( } function createRouterOptions(): RouterOptions { - const history = - __UNI_ROUTER_MODE__ === 'history' - ? createWebHistory() - : createWebHashHistory() return { - history, + history: initHistory(), strict: !!__uniConfig.router.strict, routes: __uniRoutes as RouteRecordRaw[], scrollBehavior, @@ -51,8 +47,27 @@ function createAppRouter(router: Router) { return router } +function initHistory() { + const history = + __UNI_ROUTER_MODE__ === 'history' + ? createWebHistory() + : createWebHashHistory() + history.listen((_to, from, info) => { + if (info.direction === 'back') { + const app = getApp() + const id = history.state.__id__ + if (app && id) { + ;(app.$refs.app as any).keepAliveExclude = [from + '-' + id] + } + } + }) + return history +} + const beforeEach: NavigationGuardWithThis = (to, from, next) => { - const app = getApp() - if (app) next() + next() +} +const afterEach: NavigationHookAfter = (to, from, failure) => { + console.log('afterEach.id', history.state.__id__) + console.log('afterEach', to, from, failure, JSON.stringify(history.state)) } -const afterEach: NavigationHookAfter = (to, from, failure) => {} diff --git a/packages/uni-i18n/LICENSE b/packages/uni-i18n/LICENSE new file mode 100755 index 000000000..7a4a3ea24 --- /dev/null +++ b/packages/uni-i18n/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/uni-i18n/__tests__/i18n.spec.ts b/packages/uni-i18n/__tests__/i18n.spec.ts new file mode 100644 index 000000000..9408ab951 --- /dev/null +++ b/packages/uni-i18n/__tests__/i18n.spec.ts @@ -0,0 +1,116 @@ +import { BuiltInLocale } from '../src/index' +import { I18n } from '../src/index' + +const messages = { + en: { + hello: 'the world', + helloName: 'Hello {name}', + empty: '', + 'Hello {0}': 'Hello {0}', + 'hyphen-locale': 'hello hyphen', + '1234': 'Number-based keys are found', + '1mixedKey': 'Mixed keys are not found.', + sálvame: 'save me', + }, + 'zh-Hans': { + hello: '世界', + helloName: '你好!{name}', + }, +} + +const locales: { + [name: string]: BuiltInLocale +} = { + 'zh-Hans': 'zh-Hans', + 'zh-CN': 'zh-Hans', + zh_CN: 'zh-Hans', + zh: 'zh-Hans', + zh_cn: 'zh-Hans', + zh_Hans_CN: 'zh-Hans', + 'zh-CHS': 'zh-Hans', + 'zh-Hans-CN': 'zh-Hans', + 'zh-Hans-TW': 'zh-Hans', + 'zh-CHT': 'zh-Hant', + 'zh-Hant': 'zh-Hant', + 'zh-TW': 'zh-Hant', + 'zh-Hant-TW': 'zh-Hant', + 'zh-HK': 'zh-Hant', + 'zh-Hant-HK': 'zh-Hant', + 'zh-MO': 'zh-Hant', + 'zh-Hant-MO': 'zh-Hant', + en: 'en', + 'en-SG': 'en', + 'en-US': 'en', + 'en-AU': 'en', + fr: 'fr', + 'fr-CA': 'fr', + es: 'es', + 'es-AR': 'es', + 'sv-se': 'en', //fallback +} + +describe('i18n', () => { + Object.keys(locales).forEach((name) => { + test(`locale ${name}`, () => { + const i18n = new I18n({ + locale: name as BuiltInLocale, + fallbackLocale: 'en', + messages: {}, + }) + expect(i18n.getLocale()).toBe(locales[name]) + i18n.setLocale('zh-Hant-HK') + expect(i18n.getLocale()).toBe('zh-Hant') + }) + }) + test('watchLocale', () => { + let i = 0 + function watcher(newLocale: BuiltInLocale, oldLocale: BuiltInLocale) { + i++ + expect(newLocale).toBe('zh-Hans') + expect(oldLocale).toBe('en') + } + const i18n = new I18n({ + locale: 'en', + fallbackLocale: 'en', + messages: {}, + watcher, + }) + i18n.watchLocale(watcher) + i18n.setLocale('zh') + expect(i).toBe(2) + }) + test('zh-Hans locale', () => { + const i18n = new I18n({ + locale: 'zh-Hans', + fallbackLocale: 'en', + messages, + }) + expect(i18n.t('hello')).toBe(messages['zh-Hans'].hello) + expect(i18n.t('helloName', { name: '世界' })).toBe( + messages['zh-Hans'].helloName.replace('{name}', '世界') + ) + expect(i18n.t('helloName', 'en', { name: '世界' })).toBe( + messages.en.helloName.replace('{name}', '世界') + ) + expect(i18n.t('Hello {0}', ['world'])).toBe('Hello {0}') + }) + test('en locale', () => { + const i18n = new I18n({ + locale: 'en', + fallbackLocale: 'en', + messages, + }) + expect(i18n.t('hello')).toBe(messages.en.hello) + expect(i18n.t('helloName', { name: 'world' })).toBe( + messages.en.helloName.replace('{name}', 'world') + ) + expect(i18n.t('Hello {0}', ['world'])).toBe( + messages.en['Hello {0}'].replace('{0}', 'world') + ) + expect(i18n.t('empty')).toBe(messages.en.empty) + expect(i18n.t('hyphen-locale')).toBe(messages.en['hyphen-locale']) + expect(i18n.t((1234 as unknown) as string)).toBe(messages.en[1234]) + expect(i18n.t('1mixedKey')).toBe(messages.en['1mixedKey']) + expect(i18n.t('sálvame')).toBe(messages.en['sálvame']) + }) +}) diff --git a/packages/uni-i18n/api-extractor.json b/packages/uni-i18n/api-extractor.json new file mode 100644 index 000000000..c18cb2ed3 --- /dev/null +++ b/packages/uni-i18n/api-extractor.json @@ -0,0 +1,8 @@ +{ + "extends": "../../api-extractor.json", + "mainEntryPointFilePath": "./dist/packages//src/index.d.ts", + "dtsRollup": { + "publicTrimmedFilePath": "./dist/.d.ts" + } + } + \ No newline at end of file diff --git a/packages/uni-i18n/build.json b/packages/uni-i18n/build.json new file mode 100644 index 000000000..635cb7ccf --- /dev/null +++ b/packages/uni-i18n/build.json @@ -0,0 +1,8 @@ +{ + "input": { + "src/index.ts": ["dist/uni-i18n.esm.js", "dist/uni-i18n.cjs.js"] + }, + "compilerOptions": { + "declaration": true + } +} diff --git a/packages/uni-i18n/dist/uni-i18n.cjs.js b/packages/uni-i18n/dist/uni-i18n.cjs.js new file mode 100644 index 000000000..24001a08e --- /dev/null +++ b/packages/uni-i18n/dist/uni-i18n.cjs.js @@ -0,0 +1,287 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +const isObject = (val) => val !== null && typeof val === 'object'; +class BaseFormatter { + constructor() { + this._caches = Object.create(null); + } + interpolate(message, values) { + if (!values) { + return [message]; + } + let tokens = this._caches[message]; + if (!tokens) { + tokens = parse(message); + this._caches[message] = tokens; + } + return compile(tokens, values); + } +} +const RE_TOKEN_LIST_VALUE = /^(?:\d)+/; +const RE_TOKEN_NAMED_VALUE = /^(?:\w)+/; +function parse(format) { + const tokens = []; + let position = 0; + let text = ''; + while (position < format.length) { + let char = format[position++]; + if (char === '{') { + if (text) { + tokens.push({ type: 'text', value: text }); + } + text = ''; + let sub = ''; + char = format[position++]; + while (char !== undefined && char !== '}') { + sub += char; + char = format[position++]; + } + const isClosed = char === '}'; + const type = RE_TOKEN_LIST_VALUE.test(sub) + ? 'list' + : isClosed && RE_TOKEN_NAMED_VALUE.test(sub) + ? 'named' + : 'unknown'; + tokens.push({ value: sub, type }); + } + else if (char === '%') { + // when found rails i18n syntax, skip text capture + if (format[position] !== '{') { + text += char; + } + } + else { + text += char; + } + } + text && tokens.push({ type: 'text', value: text }); + return tokens; +} +function compile(tokens, values) { + const compiled = []; + let index = 0; + const mode = Array.isArray(values) + ? 'list' + : isObject(values) + ? 'named' + : 'unknown'; + if (mode === 'unknown') { + return compiled; + } + while (index < tokens.length) { + const token = tokens[index]; + switch (token.type) { + case 'text': + compiled.push(token.value); + break; + case 'list': + compiled.push(values[parseInt(token.value, 10)]); + break; + case 'named': + if (mode === 'named') { + compiled.push(values[token.value]); + } + else { + if (process.env.NODE_ENV !== 'production') { + console.warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`); + } + } + break; + case 'unknown': + if (process.env.NODE_ENV !== 'production') { + console.warn(`Detect 'unknown' type of token!`); + } + break; + } + index++; + } + return compiled; +} + +const hasOwnProperty = Object.prototype.hasOwnProperty; +const hasOwn = (val, key) => hasOwnProperty.call(val, key); +const defaultFormatter = new BaseFormatter(); +function include(str, parts) { + return !!parts.find((part) => str.indexOf(part) !== -1); +} +function startsWith(str, parts) { + return parts.find((part) => str.indexOf(part) === 0); +} +function normalizeLocale(locale, messages) { + if (!locale) { + return; + } + locale = locale.trim().replace(/_/g, '-'); + if (messages[locale]) { + return locale; + } + locale = locale.toLowerCase(); + if (locale.indexOf('zh') === 0) { + if (locale.indexOf('-hans') !== -1) { + return 'zh-Hans'; + } + if (locale.indexOf('-hant') !== -1) { + return 'zh-Hant'; + } + if (include(locale, ['-tw', '-hk', '-mo', '-cht'])) { + return 'zh-Hant'; + } + return 'zh-Hans'; + } + const lang = startsWith(locale, ['en', 'fr', 'es']); + if (lang) { + return lang; + } +} +class I18n { + constructor({ locale, fallbackLocale, messages, watcher, formater, }) { + this.locale = 'en'; + this.fallbackLocale = 'en'; + this.message = {}; + this.messages = {}; + this.watchers = []; + if (fallbackLocale) { + this.fallbackLocale = fallbackLocale; + } + this.formater = formater || defaultFormatter; + this.messages = messages; + this.setLocale(locale); + if (watcher) { + this.watchLocale(watcher); + } + } + setLocale(locale) { + const oldLocale = this.locale; + this.locale = normalizeLocale(locale, this.messages) || this.fallbackLocale; + this.message = this.messages[this.locale]; + this.watchers.forEach((watcher) => { + watcher(this.locale, oldLocale); + }); + } + getLocale() { + return this.locale; + } + watchLocale(fn) { + const index = this.watchers.push(fn) - 1; + return () => { + this.watchers.splice(index, 1); + }; + } + mergeLocaleMessage(locale, message) { + if (this.messages[locale]) { + Object.assign(this.messages[locale], message); + } + else { + this.messages[locale] = message; + } + } + t(key, locale, values) { + let message = this.message; + if (typeof locale === 'string') { + locale = normalizeLocale(locale, this.messages); + locale && (message = this.messages[locale]); + } + else { + values = locale; + } + if (!hasOwn(message, key)) { + console.warn(`Cannot translate the value of keypath ${key}. Use the value of keypath as default.`); + return key; + } + return this.formater.interpolate(message[key], values).join(''); + } +} + +function initLocaleWatcher(appVm, i18n) { + appVm.$i18n && + appVm.$i18n.vm.$watch('locale', (newLocale) => { + i18n.setLocale(newLocale); + }, { + immediate: true, + }); +} +function getDefaultLocale() { + if (typeof navigator !== 'undefined') { + return navigator.userLanguage || navigator.language; + } + if (typeof plus !== 'undefined') { + // TODO 待调整为最新的获取语言代码 + return plus.os.language; + } + return uni.getSystemInfoSync().language; +} +function initVueI18n(messages, fallbackLocale = 'en', locale) { + const i18n = new I18n({ + locale: locale || fallbackLocale, + fallbackLocale, + messages, + }); + let t = (key, values) => { + if (typeof getApp !== 'function') { + // app-plus view + /* eslint-disable no-func-assign */ + t = function (key, values) { + return i18n.t(key, values); + }; + } + else { + const appVm = getApp().$vm; + if (!appVm.$t || !appVm.$i18n) { + if (!locale) { + i18n.setLocale(getDefaultLocale()); + } + /* eslint-disable no-func-assign */ + t = function (key, values) { + return i18n.t(key, values); + }; + } + else { + initLocaleWatcher(appVm, i18n); + /* eslint-disable no-func-assign */ + t = function (key, values) { + const $i18n = appVm.$i18n; + const silentTranslationWarn = $i18n.silentTranslationWarn; + $i18n.silentTranslationWarn = true; + const msg = appVm.$t(key, values); + $i18n.silentTranslationWarn = silentTranslationWarn; + if (msg !== key) { + return msg; + } + return i18n.t(key, $i18n.locale, values); + }; + } + } + return t(key, values); + }; + return { + t(key, values) { + return t(key, values); + }, + getLocale() { + return i18n.getLocale(); + }, + setLocale(newLocale) { + return i18n.setLocale(newLocale); + }, + mixin: { + beforeCreate() { + const unwatch = i18n.watchLocale(() => { + this.$forceUpdate(); + }); + this.$once('hook:beforeDestroy', function () { + unwatch(); + }); + }, + methods: { + $$t(key, values) { + return t(key, values); + }, + }, + }, + }; +} + +exports.I18n = I18n; +exports.initVueI18n = initVueI18n; diff --git a/packages/uni-i18n/dist/uni-i18n.d.ts b/packages/uni-i18n/dist/uni-i18n.d.ts new file mode 100644 index 000000000..a0a56d35c --- /dev/null +++ b/packages/uni-i18n/dist/uni-i18n.d.ts @@ -0,0 +1,50 @@ + +export declare type BuiltInLocale = 'zh-Hans' | 'zh-Hant' | 'en' | 'fr' | 'es'; + +export declare interface Formatter { + interpolate: (message: string, values?: Record | Array) => Array; +} + +export declare class I18n { + private locale; + private fallbackLocale; + private message; + private messages; + private watchers; + private formater; + constructor({ locale, fallbackLocale, messages, watcher, formater, }: I18nOptions); + setLocale(locale: string): void; + getLocale(): BuiltInLocale; + watchLocale(fn: LocaleWatcher): () => void; + mergeLocaleMessage(locale: BuiltInLocale, message: Record): void; + t(key: string, values?: Record | Array | BuiltInLocale): string; + t(key: string, locale?: BuiltInLocale, values?: Record | Array): string; +} + +export declare interface I18nOptions { + locale: BuiltInLocale; + fallbackLocale?: BuiltInLocale; + messages: LocaleMessages; + formater?: Formatter; + watcher?: LocaleWatcher; +} + +export declare function initVueI18n(messages: LocaleMessages, fallbackLocale?: BuiltInLocale, locale?: BuiltInLocale): { + t(key: string, values?: Record | unknown[] | undefined): string; + getLocale(): BuiltInLocale; + setLocale(newLocale: BuiltInLocale): void; + mixin: { + beforeCreate(): void; + methods: { + $$t(key: string, values?: any): string; + }; + }; +}; + +export declare type LocaleMessages = { + [name in BuiltInLocale]?: Record; +}; + +export declare type LocaleWatcher = (newLocale: BuiltInLocale, oldLocale: BuiltInLocale) => void; + +export { } diff --git a/packages/uni-i18n/dist/uni-i18n.esm.js b/packages/uni-i18n/dist/uni-i18n.esm.js new file mode 100644 index 000000000..03b3d0fc9 --- /dev/null +++ b/packages/uni-i18n/dist/uni-i18n.esm.js @@ -0,0 +1,282 @@ +const isObject = (val) => val !== null && typeof val === 'object'; +class BaseFormatter { + constructor() { + this._caches = Object.create(null); + } + interpolate(message, values) { + if (!values) { + return [message]; + } + let tokens = this._caches[message]; + if (!tokens) { + tokens = parse(message); + this._caches[message] = tokens; + } + return compile(tokens, values); + } +} +const RE_TOKEN_LIST_VALUE = /^(?:\d)+/; +const RE_TOKEN_NAMED_VALUE = /^(?:\w)+/; +function parse(format) { + const tokens = []; + let position = 0; + let text = ''; + while (position < format.length) { + let char = format[position++]; + if (char === '{') { + if (text) { + tokens.push({ type: 'text', value: text }); + } + text = ''; + let sub = ''; + char = format[position++]; + while (char !== undefined && char !== '}') { + sub += char; + char = format[position++]; + } + const isClosed = char === '}'; + const type = RE_TOKEN_LIST_VALUE.test(sub) + ? 'list' + : isClosed && RE_TOKEN_NAMED_VALUE.test(sub) + ? 'named' + : 'unknown'; + tokens.push({ value: sub, type }); + } + else if (char === '%') { + // when found rails i18n syntax, skip text capture + if (format[position] !== '{') { + text += char; + } + } + else { + text += char; + } + } + text && tokens.push({ type: 'text', value: text }); + return tokens; +} +function compile(tokens, values) { + const compiled = []; + let index = 0; + const mode = Array.isArray(values) + ? 'list' + : isObject(values) + ? 'named' + : 'unknown'; + if (mode === 'unknown') { + return compiled; + } + while (index < tokens.length) { + const token = tokens[index]; + switch (token.type) { + case 'text': + compiled.push(token.value); + break; + case 'list': + compiled.push(values[parseInt(token.value, 10)]); + break; + case 'named': + if (mode === 'named') { + compiled.push(values[token.value]); + } + else { + if (process.env.NODE_ENV !== 'production') { + console.warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`); + } + } + break; + case 'unknown': + if (process.env.NODE_ENV !== 'production') { + console.warn(`Detect 'unknown' type of token!`); + } + break; + } + index++; + } + return compiled; +} + +const hasOwnProperty = Object.prototype.hasOwnProperty; +const hasOwn = (val, key) => hasOwnProperty.call(val, key); +const defaultFormatter = new BaseFormatter(); +function include(str, parts) { + return !!parts.find((part) => str.indexOf(part) !== -1); +} +function startsWith(str, parts) { + return parts.find((part) => str.indexOf(part) === 0); +} +function normalizeLocale(locale, messages) { + if (!locale) { + return; + } + locale = locale.trim().replace(/_/g, '-'); + if (messages[locale]) { + return locale; + } + locale = locale.toLowerCase(); + if (locale.indexOf('zh') === 0) { + if (locale.indexOf('-hans') !== -1) { + return 'zh-Hans'; + } + if (locale.indexOf('-hant') !== -1) { + return 'zh-Hant'; + } + if (include(locale, ['-tw', '-hk', '-mo', '-cht'])) { + return 'zh-Hant'; + } + return 'zh-Hans'; + } + const lang = startsWith(locale, ['en', 'fr', 'es']); + if (lang) { + return lang; + } +} +class I18n { + constructor({ locale, fallbackLocale, messages, watcher, formater, }) { + this.locale = 'en'; + this.fallbackLocale = 'en'; + this.message = {}; + this.messages = {}; + this.watchers = []; + if (fallbackLocale) { + this.fallbackLocale = fallbackLocale; + } + this.formater = formater || defaultFormatter; + this.messages = messages; + this.setLocale(locale); + if (watcher) { + this.watchLocale(watcher); + } + } + setLocale(locale) { + const oldLocale = this.locale; + this.locale = normalizeLocale(locale, this.messages) || this.fallbackLocale; + this.message = this.messages[this.locale]; + this.watchers.forEach((watcher) => { + watcher(this.locale, oldLocale); + }); + } + getLocale() { + return this.locale; + } + watchLocale(fn) { + const index = this.watchers.push(fn) - 1; + return () => { + this.watchers.splice(index, 1); + }; + } + mergeLocaleMessage(locale, message) { + if (this.messages[locale]) { + Object.assign(this.messages[locale], message); + } + else { + this.messages[locale] = message; + } + } + t(key, locale, values) { + let message = this.message; + if (typeof locale === 'string') { + locale = normalizeLocale(locale, this.messages); + locale && (message = this.messages[locale]); + } + else { + values = locale; + } + if (!hasOwn(message, key)) { + console.warn(`Cannot translate the value of keypath ${key}. Use the value of keypath as default.`); + return key; + } + return this.formater.interpolate(message[key], values).join(''); + } +} + +function initLocaleWatcher(appVm, i18n) { + appVm.$i18n && + appVm.$i18n.vm.$watch('locale', (newLocale) => { + i18n.setLocale(newLocale); + }, { + immediate: true, + }); +} +function getDefaultLocale() { + if (typeof navigator !== 'undefined') { + return navigator.userLanguage || navigator.language; + } + if (typeof plus !== 'undefined') { + // TODO 待调整为最新的获取语言代码 + return plus.os.language; + } + return uni.getSystemInfoSync().language; +} +function initVueI18n(messages, fallbackLocale = 'en', locale) { + const i18n = new I18n({ + locale: locale || fallbackLocale, + fallbackLocale, + messages, + }); + let t = (key, values) => { + if (typeof getApp !== 'function') { + // app-plus view + /* eslint-disable no-func-assign */ + t = function (key, values) { + return i18n.t(key, values); + }; + } + else { + const appVm = getApp().$vm; + if (!appVm.$t || !appVm.$i18n) { + if (!locale) { + i18n.setLocale(getDefaultLocale()); + } + /* eslint-disable no-func-assign */ + t = function (key, values) { + return i18n.t(key, values); + }; + } + else { + initLocaleWatcher(appVm, i18n); + /* eslint-disable no-func-assign */ + t = function (key, values) { + const $i18n = appVm.$i18n; + const silentTranslationWarn = $i18n.silentTranslationWarn; + $i18n.silentTranslationWarn = true; + const msg = appVm.$t(key, values); + $i18n.silentTranslationWarn = silentTranslationWarn; + if (msg !== key) { + return msg; + } + return i18n.t(key, $i18n.locale, values); + }; + } + } + return t(key, values); + }; + return { + t(key, values) { + return t(key, values); + }, + getLocale() { + return i18n.getLocale(); + }, + setLocale(newLocale) { + return i18n.setLocale(newLocale); + }, + mixin: { + beforeCreate() { + const unwatch = i18n.watchLocale(() => { + this.$forceUpdate(); + }); + this.$once('hook:beforeDestroy', function () { + unwatch(); + }); + }, + methods: { + $$t(key, values) { + return t(key, values); + }, + }, + }, + }; +} + +export { I18n, initVueI18n }; diff --git a/packages/uni-i18n/package.json b/packages/uni-i18n/package.json new file mode 100644 index 000000000..325807ea6 --- /dev/null +++ b/packages/uni-i18n/package.json @@ -0,0 +1,21 @@ +{ + "name": "@dcloudio/uni-i18n", + "version": "0.0.5", + "description": "@dcloudio/uni-i18n", + "main": "dist/uni-i18n.cjs.js", + "module": "dist/uni-i18n.esm.js", + "types": "dist/uni-i18n.d.ts", + "files": [ + "dist" + ], + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/dcloudio/uni-app.git", + "directory": "packages/uni-i18n" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/dcloudio/uni-app/issues" + } +} diff --git a/packages/uni-i18n/src/I18n.ts b/packages/uni-i18n/src/I18n.ts new file mode 100644 index 000000000..2a30a8d6e --- /dev/null +++ b/packages/uni-i18n/src/I18n.ts @@ -0,0 +1,150 @@ +import BaseFormatter from './format' + +// 中文 (简体),中文 (繁體),英语,法语,西班牙语 +export type BuiltInLocale = 'zh-Hans' | 'zh-Hant' | 'en' | 'fr' | 'es' + +export type LocaleMessages = { + [name in BuiltInLocale]?: Record +} + +export interface Formatter { + interpolate: ( + message: string, + values?: Record | Array + ) => Array +} + +export type LocaleWatcher = ( + newLocale: BuiltInLocale, + oldLocale: BuiltInLocale +) => void + +export interface I18nOptions { + locale: BuiltInLocale + fallbackLocale?: BuiltInLocale + messages: LocaleMessages + formater?: Formatter + watcher?: LocaleWatcher +} + +const hasOwnProperty = Object.prototype.hasOwnProperty +const hasOwn = (val: object, key: string | symbol): key is keyof typeof val => + hasOwnProperty.call(val, key) + +const defaultFormatter = new BaseFormatter() + +function include(str: string, parts: string[]) { + return !!parts.find((part) => str.indexOf(part) !== -1) +} +function startsWith(str: string, parts: string[]) { + return parts.find((part) => str.indexOf(part) === 0) +} + +function normalizeLocale( + locale: string, + messages: LocaleMessages +): BuiltInLocale | undefined { + if (!locale) { + return + } + locale = locale.trim().replace(/_/g, '-') + if (messages[locale as BuiltInLocale]) { + return locale as BuiltInLocale + } + locale = locale.toLowerCase() + if (locale.indexOf('zh') === 0) { + if (locale.indexOf('-hans') !== -1) { + return 'zh-Hans' + } + if (locale.indexOf('-hant') !== -1) { + return 'zh-Hant' + } + if (include(locale, ['-tw', '-hk', '-mo', '-cht'])) { + return 'zh-Hant' + } + return 'zh-Hans' + } + const lang = startsWith(locale, ['en', 'fr', 'es']) + if (lang) { + return lang as BuiltInLocale + } +} + +export class I18n { + private locale: BuiltInLocale = 'en' + private fallbackLocale: BuiltInLocale = 'en' + private message: Record = {} + private messages: LocaleMessages = {} + private watchers: LocaleWatcher[] = [] + private formater: Formatter + constructor({ + locale, + fallbackLocale, + messages, + watcher, + formater, + }: I18nOptions) { + if (fallbackLocale) { + this.fallbackLocale = fallbackLocale + } + this.formater = formater || defaultFormatter + this.messages = messages + this.setLocale(locale) + if (watcher) { + this.watchLocale(watcher) + } + } + setLocale(locale: string) { + const oldLocale = this.locale + this.locale = normalizeLocale(locale, this.messages) || this.fallbackLocale + this.message = this.messages[this.locale]! + this.watchers.forEach((watcher) => { + watcher(this.locale, oldLocale) + }) + } + getLocale() { + return this.locale + } + watchLocale(fn: LocaleWatcher) { + const index = this.watchers.push(fn) - 1 + return () => { + this.watchers.splice(index, 1) + } + } + mergeLocaleMessage(locale: BuiltInLocale, message: Record) { + if (this.messages[locale]) { + Object.assign(this.messages[locale], message) + } else { + this.messages[locale] = message + } + } + t( + key: string, + values?: Record | Array | BuiltInLocale + ): string + t( + key: string, + locale?: BuiltInLocale, + values?: Record | Array + ): string + t( + key: string, + locale?: BuiltInLocale, + values?: Record | Array + ) { + let message = this.message + if (typeof locale === 'string') { + locale = normalizeLocale(locale, this.messages) + locale && (message = this.messages[locale]!) + } else { + values = locale + } + if (!hasOwn(message, key)) { + console.warn( + `Cannot translate the value of keypath ${key}. Use the value of keypath as default.` + ) + return key + } + return this.formater.interpolate(message[key], values).join('') + } +} diff --git a/packages/uni-i18n/src/format.ts b/packages/uni-i18n/src/format.ts new file mode 100644 index 000000000..ab6b5921b --- /dev/null +++ b/packages/uni-i18n/src/format.ts @@ -0,0 +1,123 @@ +const isObject = (val: unknown): val is Record => + val !== null && typeof val === 'object' + +export default class BaseFormatter { + _caches: { [key: string]: Array } + + constructor() { + this._caches = Object.create(null) + } + + interpolate( + message: string, + values?: Record | Array + ): Array { + if (!values) { + return [message] + } + let tokens: Array = this._caches[message] + if (!tokens) { + tokens = parse(message) + this._caches[message] = tokens + } + return compile(tokens, values) + } +} + +type Token = { + type: 'text' | 'named' | 'list' | 'unknown' + value: string +} + +const RE_TOKEN_LIST_VALUE: RegExp = /^(?:\d)+/ +const RE_TOKEN_NAMED_VALUE: RegExp = /^(?:\w)+/ + +export function parse(format: string): Array { + const tokens: Array = [] + let position: number = 0 + + let text: string = '' + while (position < format.length) { + let char: string = format[position++] + if (char === '{') { + if (text) { + tokens.push({ type: 'text', value: text }) + } + + text = '' + let sub: string = '' + char = format[position++] + while (char !== undefined && char !== '}') { + sub += char + char = format[position++] + } + const isClosed = char === '}' + + const type = RE_TOKEN_LIST_VALUE.test(sub) + ? 'list' + : isClosed && RE_TOKEN_NAMED_VALUE.test(sub) + ? 'named' + : 'unknown' + tokens.push({ value: sub, type }) + } else if (char === '%') { + // when found rails i18n syntax, skip text capture + if (format[position] !== '{') { + text += char + } + } else { + text += char + } + } + + text && tokens.push({ type: 'text', value: text }) + + return tokens +} + +export function compile( + tokens: Array, + values: Record | Array +): Array { + const compiled: Array = [] + let index: number = 0 + + const mode: string = Array.isArray(values) + ? 'list' + : isObject(values) + ? 'named' + : 'unknown' + if (mode === 'unknown') { + return compiled + } + + while (index < tokens.length) { + const token: Token = tokens[index] + switch (token.type) { + case 'text': + compiled.push(token.value) + break + case 'list': + compiled.push(values[parseInt(token.value, 10)]) + break + case 'named': + if (mode === 'named') { + compiled.push(values[token.value]) + } else { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `Type of token '${token.type}' and format of value '${mode}' don't match!` + ) + } + } + break + case 'unknown': + if (process.env.NODE_ENV !== 'production') { + console.warn(`Detect 'unknown' type of token!`) + } + break + } + index++ + } + + return compiled +} diff --git a/packages/uni-i18n/src/index.ts b/packages/uni-i18n/src/index.ts new file mode 100644 index 000000000..24c83003c --- /dev/null +++ b/packages/uni-i18n/src/index.ts @@ -0,0 +1,2 @@ +export * from './I18n' +export * from './vue-i18n' diff --git a/packages/uni-i18n/src/vue-i18n.ts b/packages/uni-i18n/src/vue-i18n.ts new file mode 100644 index 000000000..630278b48 --- /dev/null +++ b/packages/uni-i18n/src/vue-i18n.ts @@ -0,0 +1,103 @@ +import { I18n, BuiltInLocale, LocaleMessages } from './I18n' + +type Interpolate = ( + key: string, + values?: Record | Array +) => string + +function initLocaleWatcher(appVm: any, i18n: I18n) { + appVm.$i18n && + appVm.$i18n.vm.$watch( + 'locale', + (newLocale: BuiltInLocale) => { + i18n.setLocale(newLocale) + }, + { + immediate: true, + } + ) +} + +function getDefaultLocale() { + if (typeof navigator !== 'undefined') { + return (navigator as any).userLanguage || navigator.language + } + if (typeof plus !== 'undefined') { + // TODO 待调整为最新的获取语言代码 + return plus.os.language + } + return uni.getSystemInfoSync().language +} + +export function initVueI18n( + messages: LocaleMessages, + fallbackLocale: BuiltInLocale = 'en', + locale?: BuiltInLocale +) { + const i18n = new I18n({ + locale: locale || fallbackLocale, + fallbackLocale, + messages, + }) + let t: Interpolate = (key, values) => { + if (typeof getApp !== 'function') { + // app-plus view + /* eslint-disable no-func-assign */ + t = function (key, values) { + return i18n.t(key, values) + } + } else { + const appVm = getApp().$vm + if (!appVm.$t || !appVm.$i18n) { + if (!locale) { + i18n.setLocale(getDefaultLocale()) + } + /* eslint-disable no-func-assign */ + t = function (key, values) { + return i18n.t(key, values) + } + } else { + initLocaleWatcher(appVm, i18n) + /* eslint-disable no-func-assign */ + t = function (key, values) { + const $i18n = appVm.$i18n + const silentTranslationWarn = $i18n.silentTranslationWarn + $i18n.silentTranslationWarn = true + const msg = appVm.$t(key, values) + $i18n.silentTranslationWarn = silentTranslationWarn + if (msg !== key) { + return msg + } + return i18n.t(key, $i18n.locale, values) + } + } + } + return t(key, values) + } + return { + t(key: string, values?: Record | Array) { + return t(key, values) + }, + getLocale() { + return i18n.getLocale() + }, + setLocale(newLocale: BuiltInLocale) { + return i18n.setLocale(newLocale) + }, + mixin: { + beforeCreate() { + const unwatch = i18n.watchLocale(() => { + ;(this as any).$forceUpdate() + }) + ;(this as any).$once('hook:beforeDestroy', function () { + unwatch() + }) + }, + methods: { + $$t(key: string, values?: any) { + return t(key, values) + }, + }, + }, + } +} diff --git a/packages/uni-i18n/tsconfig.json b/packages/uni-i18n/tsconfig.json new file mode 100644 index 000000000..843865ac8 --- /dev/null +++ b/packages/uni-i18n/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "dist", + "sourceMap": false, + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "allowJs": false, + "strict": true, + "noUnusedLocals": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "removeComments": false, + "lib": ["ESNext", "DOM"] + }, + "include": ["src"] +} diff --git a/rollup.config.js b/rollup.config.js index 442cace90..e7891bd49 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -28,6 +28,7 @@ Object.keys(buildOptions.input).forEach((name) => { createConfig(name, { file: resolve(file), format: file.includes('.cjs.') ? 'cjs' : 'es', + exports: 'auto', }) ) }) @@ -36,6 +37,7 @@ Object.keys(buildOptions.input).forEach((name) => { createConfig(name, { file: resolve(buildOptions.input[name]), format: (buildOptions.output && buildOptions.output.format) || `es`, + exports: 'auto', }) ) } diff --git a/scripts/build.js b/scripts/build.js index 257b1d0a2..ba6fe98a1 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -46,7 +46,10 @@ async function build(target) { } const bundler = pkg.buildOptions && pkg.buildOptions.bundler - const types = target === 'uni-shared' || (buildTypes && pkg.types) + const types = + target === 'uni-shared' || + target === 'uni-i18n' || + (buildTypes && pkg.types) // if building a specific format, do not remove dist. if (!formats && bundler !== 'vite') { await fs.remove(`${pkgDir}/dist`) -- GitLab