diff --git a/.github/workflows/affine-keck.yml b/.github/workflows/affine-keck.yml.bak similarity index 100% rename from .github/workflows/affine-keck.yml rename to .github/workflows/affine-keck.yml.bak 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/.github/workflows/livedemo.yml b/.github/workflows/livedemo.yml new file mode 100644 index 0000000000..d5ed5b9987 --- /dev/null +++ b/.github/workflows/livedemo.yml @@ -0,0 +1,87 @@ +name: Build AFFiNE-Local + +on: + push: + branches: [master] + # pull_request: + # branches: [master] + +# Cancels all previous workflow runs for pull requests that have not completed. +# See https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + # The concurrency group contains the workflow name and the branch name for + # pull requests or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + NAMESPACE: toeverything + AFFINE_IMAGE_NAME: LIVEDEMO + IMAGE_TAG_LATEST: nightly-latest + LOCAL_CACHE: localhost:5000/toeverything/relocate:latest + +jobs: + ligo-virgo: + runs-on: self-hosted + environment: development + permissions: + contents: read + packages: write + + services: + registry: + image: registry:2 + ports: + - 5000:5000 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + with: + driver-opts: network=host + + - name: Extract metadata (tags, labels) for Docker (AFFiNE-Local) + id: meta_affine + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.AFFINE_IMAGE_NAME }} + tags: ${{ env.IMAGE_TAG_LATEST }} + + - name: Build and push Docker image (AFFINE-Local) + uses: docker/build-push-action@v3 + env: + HUBSPOT_API_SECRET: ${{ secrets.SuperSecret }} + with: + context: . + push: true + file: ./.github/deployment/Dockerfile-affine + tags: ${{ env.LOCAL_CACHE }} + target: AFFiNE + + - name: Build and push Docker image (AFFINE-Local) + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64,linux/arm64/v8 + file: ./.github/deployment/Dockerfile-affine-multiplatform + push: ${{ github.ref == 'refs/heads/master' && true || false }} + tags: ${{ steps.meta_affine.outputs.tags }} + labels: ${{ steps.meta_affine.outputs.labels }} + target: AFFiNE + build-args: | + BASE_IMAGE=${{ env.LOCAL_CACHE }} diff --git a/apps/ligo-virgo/src/template.html b/apps/ligo-virgo/src/template.html index d0c4a84e0b..badce60082 100644 --- a/apps/ligo-virgo/src/template.html +++ b/apps/ligo-virgo/src/template.html @@ -4,6 +4,7 @@ <%= htmlWebpackPlugin.options.title %> + <%= htmlWebpackPlugin.options.hubspotScript %>
diff --git a/apps/ligo-virgo/webpack.config.js b/apps/ligo-virgo/webpack.config.js index 11be2e1c76..377f32551a 100644 --- a/apps/ligo-virgo/webpack.config.js +++ b/apps/ligo-virgo/webpack.config.js @@ -12,6 +12,13 @@ const Style9Plugin = require('style9/webpack'); const enableBundleAnalyzer = process.env.BUNDLE_ANALYZER; +function generateHubspotScript() { + const hubspotApiSecret = process.env.HUBSPOT_API_SECRET; + if (hubspotApiSecret) { + return ``; + } +} + module.exports = function (webpackConfig) { const config = getNxWebpackConfig(webpackConfig); @@ -175,6 +182,7 @@ module.exports = function (webpackConfig) { ), //favicon path template: path.resolve(__dirname, './src/template.html'), publicPath: '/', + hubspotScript: generateHubspotScript(), }), new Style9Plugin(), isProd && diff --git a/apps/venus/src/app/IndexPage/Alternatives.tsx b/apps/venus/src/app/IndexPage/Alternatives.tsx index 17403e01c9..4c7645a9d8 100644 --- a/apps/venus/src/app/IndexPage/Alternatives.tsx +++ b/apps/venus/src/app/IndexPage/Alternatives.tsx @@ -7,7 +7,7 @@ import { useMediaQuery } from '@mui/material'; const Alternatives = styled(Box)<{ width: string }>(({ width }) => ({ position: 'relative', - width: '20em', + width: '24em', height: '128px', transform: 'translateY(-8px)', overflowY: 'hidden', @@ -21,7 +21,7 @@ const Alternatives = styled(Box)<{ width: string }>(({ width }) => ({ height: 'inherit', position: 'absolute', left: '0', - top: '0', + top: '-23px', paddingTop: '22px', lineHeight: '96px', '@media (max-width: 1024px)': { @@ -105,7 +105,7 @@ export const AlternativesProduct = () => { )} > { }} > - + - + - + - + - + + + + + setDrawerState(false)} + > + + + navigate('/aboutus')}> + + + + window.open('https://blog.affine.pro') + } + > + + + + window.open('https://docs.affine.pro/') + } + > + + + + window.open('https://feedback.affine.pro/') + } + > + + + + + {contactUsCollapse ? ( + + ) : ( + + )} + + + + window.open('https://discord.gg/Arn7TqJBvG') + } + > + + + + window.open('https://t.me/affineworkos') + } + > + + + + window.open( + 'https://www.reddit.com/r/Affine/' + ) + } + > + + + + window.open( + 'https://medium.com/@affineworkos' + ) + } + > + + + + window.open( + 'mailto:contact@toeverything.info' + ) + } + > + + + + + + {languageCollapse ? : } + + + i18n.changeLanguage('en')} + > + + + + i18n.changeLanguage('zh')} + > + + + + + + + + + ); +}; + +const Container = styled(Grid)({ + position: 'fixed', + top: 0, + left: '50%', + transform: 'translateX(-50%)', + width: '100%', + paddingTop: '1em', + backgroundColor: '#fff', + zIndex: 1500, + maxWidth: '1440px', + margin: 'auto', + marginTop: '0 !important', +}); + +const StyledImage = styled('img')({ + height: '24px', + marginRight: '16px', + cursor: 'pointer', +}); + +const StyledDrawerContainer = styled('div')({ + paddingTop: '72px', +}); diff --git a/apps/venus/src/app/i18n/resources/en.json b/apps/venus/src/app/i18n/resources/en.json index 789a577584..80381df1d7 100644 --- a/apps/venus/src/app/i18n/resources/en.json +++ b/apps/venus/src/app/i18n/resources/en.json @@ -10,6 +10,7 @@ "Alternative": "Alternative", "Check GitHub": "Check GitHub", "Try it Online": "Try it Online", + "language": "Language", "description1": { "part1": "Affine is the next-generation collaborative knowledge base for professionals.", "part2": "It's not just a collection of Docs, whiteboard, and tables.", diff --git a/apps/venus/src/app/i18n/resources/zh.json b/apps/venus/src/app/i18n/resources/zh.json index 9a3e51a2ca..545a644c28 100644 --- a/apps/venus/src/app/i18n/resources/zh.json +++ b/apps/venus/src/app/i18n/resources/zh.json @@ -10,6 +10,7 @@ "Alternative": "的另一种选择", "Check GitHub": "GitHub中查看", "Try it Online": "在线试用", + "language": "语言", "description1": { "part1": "Affine是面向专业人士的下一代协同知识库", "part2": "它不仅仅是一个文档、白板和表格的集合。", 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: