diff --git a/.github/workflows/lint.yml b/.github/workflows/languages-sync.yml similarity index 52% rename from .github/workflows/lint.yml rename to .github/workflows/languages-sync.yml index 8efce5cbd4..33b0b79e93 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/languages-sync.yml @@ -1,10 +1,17 @@ -name: Lint +name: Languages Sync on: push: - branches: [master] - # pull_request: - # branches: [master] + branches: [ "develop", "master" ] + paths: + - 'libs/datasource/i18n/**' + - '.github/workflows/languages-sync.yml' + pull_request: + branches: [ "develop", "master" ] + paths: + - 'libs/datasource/i18n/**' + - '.github/workflows/languages-sync.yml' + workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. # See https://docs.github.com/en/actions/using-jobs/using-concurrency @@ -18,11 +25,9 @@ jobs: main: strategy: matrix: - node-version: [16] + node-version: [18] os: [ubuntu-latest] runs-on: ${{ matrix.os }} - # TODO Remove the next line after cleaning all errors - continue-on-error: true steps: - name: Checkout @@ -43,20 +48,16 @@ jobs: - name: Install node modules run: pnpm install - - name: Lint - if: always() - run: pnpm run lint:with-cache + - name: Check Language Key + if: github.ref != 'refs/heads/develop' && github.ref != 'refs/heads/master' + working-directory: ./libs/datasource/i18n + run: pnpm run sync-languages:check + env: + TOLGEE_API_KEY: ${{ secrets.TOLGEE_API_KEY }} - # - name: Check - # if: always() - # run: pnpm run check - - # - name: Format Check - # if: always() - # run: pnpm run format:ci - - # - name: Build - # run: pnpm run build - - # - name: Test - # run: pnpm run test + - name: Sync Languages + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' + working-directory: ./libs/datasource/i18n + run: pnpm run sync-languages + env: + TOLGEE_API_KEY: ${{ secrets.TOLGEE_API_KEY }} diff --git a/libs/datasource/i18n/README.md b/libs/datasource/i18n/README.md index ff902b333c..c454be380a 100644 --- a/libs/datasource/i18n/README.md +++ b/libs/datasource/i18n/README.md @@ -8,7 +8,7 @@ import { useTranslation } from '@toeverything/datasource/i18n'; // base.json // { // 'Text': 'some text', -// 'Switch to language': 'Switch to {language}', +// 'Switch to language': 'Switch to {{language}}', // <- you can interpolation by curly brackets // }; const App = () => { @@ -41,10 +41,12 @@ const App = () => { export TOLGEE_API_KEY=tgpak_XXXXXXX ``` -- Run the `sync` script from package script +- Run the `sync-languages:check` to check all languages +- Run the `sync-languages` script to add new keys to the tolgee platform ## References +- [AFFiNE | Tolgee](https://i18n.affine.pro/) +- [Tolgee Documentation](https://tolgee.io/docs/) - [i18next](https://www.i18next.com/) - [react-i18next](https://react.i18next.com/) -- [Tolgee](https://tolgee.io/docs/) diff --git a/libs/datasource/i18n/package.json b/libs/datasource/i18n/package.json index 8f6afe9b85..eb1657865e 100644 --- a/libs/datasource/i18n/package.json +++ b/libs/datasource/i18n/package.json @@ -2,11 +2,11 @@ "name": "@toeverything/datasource/i18n", "version": "0.0.1", "scripts": { - "sync": "NODE_OPTIONS=--experimental-fetch ts-node src/scripts/sync.ts" + "sync-languages": "NODE_OPTIONS=--experimental-fetch ts-node src/scripts/sync.ts", + "sync-languages:check": "pnpm run sync-languages --check" }, "dependencies": { "i18next": "^21.9.1", - "jotai": "^1.8.1", "react-i18next": "^11.18.4" } } diff --git a/libs/datasource/i18n/src/index.ts b/libs/datasource/i18n/src/index.ts index 187ed40aae..a421494bf8 100644 --- a/libs/datasource/i18n/src/index.ts +++ b/libs/datasource/i18n/src/index.ts @@ -19,6 +19,8 @@ declare module 'react-i18next' { } } +const STORAGE_KEY = 'i18n_lng'; + const LOCALES = [ { value: 'en', text: 'English', res: en_US }, { value: 'zh', text: '简体中文', res: zh_CN }, @@ -29,11 +31,26 @@ const resources = LOCALES.reduce( {} ); +const fallbackLng = LOCALES[0].value; +const standardizeLocale = (language: string) => { + if (LOCALES.find(locale => locale.value === language)) return language; + if ( + LOCALES.find( + locale => locale.value === language.slice(0, 2).toLowerCase() + ) + ) + return language; + return fallbackLng; +}; + +const language = standardizeLocale( + localStorage.getItem(STORAGE_KEY) ?? navigator.language +); + const i18n = i18next.createInstance(); i18n.use(initReactI18next).init({ - // TODO auto detect - lng: LOCALES[0].value, - fallbackLng: LOCALES[0].value, + lng: language, + fallbackLng, debug: process.env['NODE_ENV'] === 'development', resources, @@ -42,6 +59,10 @@ i18n.use(initReactI18next).init({ }, }); +i18n.on('languageChanged', lng => { + localStorage.setItem(STORAGE_KEY, lng); +}); + const I18nProvider = I18nextProvider; export { i18n, useTranslation, I18nProvider, LOCALES }; diff --git a/libs/datasource/i18n/src/scripts/request.ts b/libs/datasource/i18n/src/scripts/request.ts index 64e18bd9bd..c016909a24 100644 --- a/libs/datasource/i18n/src/scripts/request.ts +++ b/libs/datasource/i18n/src/scripts/request.ts @@ -9,6 +9,7 @@ if (!TOLGEE_API_KEY) { const withTolgee = ( fetch: typeof globalThis.fetch ): typeof globalThis.fetch => { + const baseUrl = `${TOLGEE_API_URL}/v2/projects`; const headers = new Headers({ 'X-API-Key': TOLGEE_API_KEY, 'Content-Type': 'application/json', @@ -29,14 +30,14 @@ const withTolgee = ( if (!argArray[0].headers) { argArray[0] = { ...argArray[0], - url: `${TOLGEE_API_URL}/v2/projects${argArray[0].url}`, + url: `${baseUrl}${argArray[0].url}`, headers, }; } } else { // URL or URLLike + ?RequestInit if (typeof argArray[0] === 'string') { - argArray[0] = TOLGEE_API_URL + argArray[0]; + argArray[0] = `${baseUrl}${argArray[0]}`; } if (!argArray[1]) { argArray[1] = {}; diff --git a/libs/datasource/i18n/src/scripts/sync.ts b/libs/datasource/i18n/src/scripts/sync.ts index 5b264bf329..a71a0de61f 100644 --- a/libs/datasource/i18n/src/scripts/sync.ts +++ b/libs/datasource/i18n/src/scripts/sync.ts @@ -78,6 +78,41 @@ const differenceObject = ( return { add, remove, modify }; }; +function warnDiff(diff: { add: string[]; remove: string[]; modify: string[] }) { + if (diff.add.length) { + console.log('New keys found:', diff.add.join(', ')); + //See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message + process.env['CI'] && + console.log( + `::notice file=${BASE_JSON_PATH},line=1,title=New keys::${diff.add.join( + ', ' + )}` + ); + } + if (diff.remove.length) { + console.warn('[WARN]', 'Unused keys found:', diff.remove.join(', ')); + process.env['CI'] && + console.warn( + `::notice file=${BASE_JSON_PATH},line=1,title=Unused keys::${diff.remove.join( + ', ' + )}` + ); + } + if (diff.modify.length) { + console.warn( + '[WARN]', + 'Inconsistent keys found:', + diff.modify.join(', ') + ); + process.env['CI'] && + console.warn( + `::warning file=${BASE_JSON_PATH},line=1,title=Inconsistent keys::${diff.modify.join( + ', ' + )}` + ); + } +} + const main = async () => { console.log('Loading local base translations...'); const baseLocalTranslations = JSON.parse( @@ -107,17 +142,14 @@ const main = async () => { ); console.log(''); // new line - diff.add.length && console.log('New keys found:', diff.add.join(', ')); - diff.remove.length && - console.warn('[WARN]', 'Unused keys found:', diff.remove.join(', ')); - diff.modify.length && - console.warn( - '[WARN]', - 'Inconsistent keys found:', - diff.modify.join(', ') - ); + warnDiff(diff); console.log(''); // new line + if (process.argv.slice(2).includes('--check')) { + // check mode + return; + } + diff.add.forEach(async key => { const val = flatLocalTranslations[key]; console.log(`Creating new key: ${key} -> ${val}`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52c187aa40..164e93cf73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -580,11 +580,9 @@ importers: libs/datasource/i18n: specifiers: i18next: ^21.9.1 - jotai: ^1.8.1 react-i18next: ^11.18.4 dependencies: i18next: 21.9.1 - jotai: 1.8.1 react-i18next: 11.18.4_i18next@21.9.1 libs/datasource/jwt: