Compare commits

...

52 Commits

Author SHA1 Message Date
Evgeny e026250893 1.8.1 (#34)
* bump version to 1.8.1

* update translation
2026-06-10 06:55:14 +05:00
Evgeny d2be66831d 1.8.0 (#33)
* bump version to 1.8.0

* update Dockerfile

* update translation
2026-06-02 04:59:47 +05:00
flameshikari 49472ac801 update translations
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2026-05-04 10:33:19 +05:00
flameshikari a81bd3bd17 bump version to 1.7.1 2026-05-04 10:22:47 +05:00
flameshikari f22d5952bb fix workflow permission
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2026-05-01 09:31:35 +05:00
flameshikari 02bdd461b4 fix workflow token 2026-05-01 09:28:49 +05:00
Evgeny d0abf84aa8 1.7.0 (#32)
* bump version to 1.7.0

* update HMR mode; make a single dev container for every service; etc

* update translations
2026-05-01 09:14:01 +05:00
Aleksandr Posazhennikov b3bda3622c upd: ru.json (#31)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* upd: ru.json

Заменил переводы некоторых фраз на более подходящие по смыслу и синтаксису

* return indents in ru.json

---------

Co-authored-by: flameshikari <mail@hexed.pw>
2026-04-14 00:05:10 +05:00
flameshikari 856cf4b0c9 update translations
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2026-03-21 00:11:08 +05:00
flameshikari 0bc1c14a9c bump version to 1.6.1 2026-03-21 00:10:47 +05:00
Evgeny 2c681f14f2 1.5.0 (#30)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump version to 1.5.0

* update translation.json
2026-02-16 15:13:27 +05:00
Evgeny 2914b54933 1.4.0 (#29)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump version to 1.4.0

* update .dockerignore

* update translation.json
2026-01-28 16:09:50 +05:00
Evgeny 076879fd63 1.3.0 (#28)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump version to 1.3.0

* update translation.json

* update the workflow
2026-01-21 15:49:17 +05:00
Evgeny fbe5d1adb1 1.2.0 (#27)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump version to 1.2.0

* update translation.json

* update workflows

* update dockerfiles
2026-01-08 22:30:32 +05:00
flameshikari b7c7d5a0ac bump GH Actions versions
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-11-17 18:26:44 +05:00
flameshikari 2fe50b5784 update translations 2025-11-17 18:22:42 +05:00
flameshikari 08aef9b911 bump version to 1.1.0 2025-11-17 18:20:59 +05:00
flameshikari d963ac76bf update Dockerfile*
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-10-29 19:21:40 +05:00
flameshikari afedb4b0ca update translations 2025-10-29 19:21:23 +05:00
flameshikari 2ccc82c02e bump version to 1.0.1 2025-10-29 19:21:05 +05:00
Evgeny daa7561aff 1.0.0 (#26)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump outline to 1.0.0-2

* update translation.json

* update readme.*

* update Dockerfile*

* bump outline to 1.0.0-test8

* reorganize repo, add git hook

* bump version to 1.0.0

* update translations
2025-10-27 17:47:11 +05:00
flameshikari 7bcf8279e2 update translation.json
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-09-19 11:06:49 +05:00
flameshikari bc93c415d8 bump outline to v0.87.4 2025-09-19 10:39:14 +05:00
flameshikari 7cbc22e1bf update translation.json
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-09-02 01:54:18 +05:00
flameshikari 73dd19e49b update readme and move to .github dir 2025-09-02 01:53:40 +05:00
flameshikari b9bfdc024f bump outline to v0.87.3 2025-09-02 01:46:57 +05:00
flameshikari 56c8631c5a update readme
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-09-01 01:34:31 +05:00
flameshikari 74c29e1934 add the script for bumping versions in readme 2025-09-01 01:31:09 +05:00
flameshikari d3399ba110 update translation.json 2025-09-01 00:50:15 +05:00
flameshikari c6e5a87421 update vite.patch 2025-09-01 00:50:01 +05:00
flameshikari 0675c06f23 update diff.py 2025-09-01 00:49:54 +05:00
flameshikari ddb2decad0 bump outline to v0.87.0 2025-08-31 23:13:48 +05:00
flameshikari 3a3728b9bd bump outline to v0.86.1 2025-08-11 12:23:09 +05:00
flameshikari be856bfbdc update translations
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-08-06 22:40:05 +05:00
flameshikari 82256f4ada bump outline to v0.86.0 2025-08-06 20:38:08 +05:00
flameshikari c7d513f03e bump outline to v0.85.1 2025-07-13 02:19:06 +05:00
sovushik 8d9d6c7f14 Обновление перевода (#25)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* Update translation.json

Скорректировал перевод

* Update translation.json

Еще парочка исправлений
2025-07-06 18:53:13 +05:00
flameshikari c794997b26 update readme
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-07-05 08:20:37 +05:00
Evgeny 4485a10514 hot reload (#24)
* container with hot reload

* update readme
2025-07-05 08:18:15 +05:00
flameshikari b129a74d39 update readme 2025-07-04 23:58:27 +05:00
Evgeny d22c3ab04c 0.85.0 (#23)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* bump outline to v0.85.0

* bump Node version in Dockerfile

* use new OIDC_ISSUER_URL in docker-compose.yml (v0.85.0)

* update translations

* update the workflow

* update readme
2025-07-04 14:14:52 +05:00
Evgeny cf2d26b011 0.84.0 (#19)
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
* update diff.py for handling ru plurals

* update translations

* add -m option to create home folder for npm cache

* bump outline to v0.84.0

* update translations
2025-05-12 17:26:59 +05:00
flameshikari 2c34b016fe fix Dockerfile order
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-04-12 03:35:18 +05:00
flameshikari 16ddf6f2b6 some fixes 2025-04-12 01:07:12 +05:00
Evgeny 032263ec5d 0.83.0 (#17)
* add patch for version checking of outline-ru, not the original one

* optimize Dockerfile and add docker-compose.yml for deploying on localhost for testing purposes

* make ru_RU as the default language, combine patches into one

* update the workflow

* update docker* files

* rename the script

* testing translations

* add concurrency and fix regexps in the workflow

* disable action output

* bump outline to 0.83.0

* update Dockerfile

* update translations

* update readme

* update readme

* clear trailing whitespaces
2025-04-12 00:46:50 +05:00
Evgeny e098d1a7df 0.82.0
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
refactoring and fixes
2025-03-11 22:05:24 +05:00
flameshikari 5204b57626 add patch, add nightly build, reorganize the repo
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-03-11 20:21:32 +05:00
flameshikari 2751858e48 update readme 2025-03-11 17:31:01 +05:00
flameshikari bef3b69a5e update license 2025-03-11 16:12:01 +05:00
flameshikari af60692963 remove shared dir 2025-03-11 15:39:23 +05:00
flameshikari fdb2cc6bc5 change submodule name 2025-03-11 15:36:15 +05:00
flameshikari 0070936e30 fix HEALTHCHECK
Build / Build [amd64] (push) Has been cancelled
Build / Build [arm64] (push) Has been cancelled
Build / Publish (push) Has been cancelled
2025-02-27 15:15:32 +05:00
27 changed files with 2684 additions and 1650 deletions
+16
View File
@@ -1,2 +1,18 @@
__mocks__
.git
.vscode
.github
.circleci
.DS_Store
.env*
.eslint*
.oxlintrc*
.log
Makefile
Procfile
app.json
crowdin.yml
build
docker-compose.yml
node_modules
.yarn
+6
View File
@@ -0,0 +1,6 @@
COMPOSE_PROFILES=dev
# PORT=10240
# PORT_OIDC=10241
# PORT_VITE=10242
# SECRET=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef # openssl rand -hex 32
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
WORKDIR="$(dirname "$(readlink -f "${0}")")"
if ! git diff --cached --quiet --submodule=diff -- ./outline; then
python "$WORKDIR/update_readme.py"
git add ./README.md
exit 0
else
exit 0
fi
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python
import os
import re
import json
ignore = '0.71.0'
def resolve(path):
basepath = os.path.abspath(os.path.dirname(__file__))
abspath = os.path.abspath(basepath + '/' + path)
return abspath
def get_version(path):
with open(path, 'r') as f:
data = json.load(f)
return data.get('version', None)
def replace_version(path, version):
with open(path, 'r') as file:
content = file.read()
pattern = re.compile(r'(\d+\.\d+\.\d+(-\w+)?)')
check = lambda x: ignore if ignore == x.group(0) else version
with open(path, 'w') as file:
file.write(pattern.sub(check, content))
version = get_version(resolve('../../outline/package.json'))
replace_version(resolve('../../README.md'), version)
+36 -27
View File
@@ -6,12 +6,20 @@ on:
- master
- dev
paths:
- .github/workflows/*
- shared/**
- src/**
- .github/workflows/**
- outline/**
- translation/ru.json
- Dockerfile
workflow_dispatch:
concurrency:
group: outline
env:
DOCKER_BUILD_CHECKS_ANNOTATIONS: false
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
jobs:
build:
name: Build [${{ matrix.arch }}]
@@ -26,26 +34,23 @@ jobs:
arch:
- amd64
- arm64
# - ppc64le
# - s390x
# - arm/v7
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ github.actor }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub CR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -54,10 +59,12 @@ jobs:
- name: Set Version
id: version
run: |
echo "version=$(jq -r '.version' src/package.json)" | tee -a $GITHUB_OUTPUT
version=$(jq -r '.version' outline/package.json)
[[ -z $version ]] && exit 1
echo "version=$version" | tee -a $GITHUB_OUTPUT
- name: Set Metadata
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
id: metadata
with:
images: |
@@ -65,25 +72,26 @@ jobs:
ghcr.io/${{ github.repository }}
- name: Build Image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
id: build
with:
cache-from: type=gha,scope=build-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }}
platforms: linux/${{ matrix.arch }}
build-args: |
APP_PATH=/opt/outline
SRC_PATH=./outline
labels: ${{ steps.metadata.outputs.labels }}
outputs: type=image,"name=${{ github.repository }},ghcr.io/${{ github.repository }}",push-by-digest=true,name-canonical=true,push=true
- name: Export Digests
if: ${{ github.ref == 'refs/heads/master' }}
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload Digests
uses: actions/upload-artifact@v4
if: ${{ github.ref == 'refs/heads/master' }}
uses: actions/upload-artifact@v7
with:
name: digests-linux-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
@@ -94,45 +102,45 @@ jobs:
name: Publish
runs-on: ubuntu-24.04
needs: build
if: ${{ github.ref == 'refs/heads/master' }}
permissions:
contents: read
contents: write
packages: write
env:
version: ${{ needs.build.outputs.version }}
steps:
- name: Download Digests
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ github.actor }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub CR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Set Metadata
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest
type=raw,value=${{ env.version }}
type=raw,value=nightly,enable=${{ github.ref == 'refs/heads/dev' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=${{ env.version }},enable=${{ github.ref == 'refs/heads/master' }}
- name: Create Manifest & Push
working-directory: ${{ runner.temp }}/digests
@@ -143,9 +151,10 @@ jobs:
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
- name: Create Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: ${{ github.ref == 'refs/heads/master' }}
with:
name: ${{ env.version }}
tag_name: ${{ env.version }}
body: "[Изменения в ${{ env.version }}](https://github.com/outline/outline/releases/tag/v${{ env.version }})"
token: ${{ secrets.TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
+2 -1
View File
@@ -1 +1,2 @@
scripts/translation.json
translation/*.json
!translation/ru.json
+2 -2
View File
@@ -1,3 +1,3 @@
[submodule "src"]
path = src
[submodule "outline"]
path = outline
url = https://github.com/outline/outline.git
+41 -41
View File
@@ -1,46 +1,46 @@
ARG APP_PATH=/opt/outline
ARG SRC_PATH=./outline
FROM node:20-slim AS base
FROM node:24.16.0 AS build
ARG CDN_URL
ARG APP_PATH
ARG SRC_PATH
WORKDIR $APP_PATH
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ${SRC_PATH}/.yarnrc.yml ./
COPY ${SRC_PATH}/patches ./patches
ENV NODE_OPTIONS='--max-old-space-size=24000'
RUN corepack enable && \
yarn install --immutable --network-timeout 1000000 && \
yarn cache clean
COPY ${SRC_PATH} .
COPY ./patches/lang.patch .
RUN patch -p1 < lang.patch
COPY ./translation/ru.json ./shared/i18n/locales/ru_RU/translation.json
RUN yarn build && \
yarn workspaces focus --production
FROM node:24.16.0-slim AS release
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
ENV DATA_PATH=/var/lib/outline/data
ENV USER=nodejs
RUN addgroup --gid 1001 ${USER} && \
adduser --uid 1001 --ingroup ${USER} ${USER} && \
mkdir -p ${DATA_PATH} && \
chown -R ${USER}:${USER} ${DATA_PATH}/..
ARG APP_PATH
WORKDIR $APP_PATH
FROM base AS build
COPY ./src/package.json ./src/yarn.lock ./
COPY ./src/patches ./patches
RUN yarn install --no-optional --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean
COPY src .
COPY shared ./shared
ARG CDN_URL
RUN yarn build
RUN rm -rf node_modules
RUN yarn install --production=true --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean
ENV PORT=3000
FROM base AS release
COPY --chown=${USER}:${USER} --from=build $APP_PATH/node_modules ./node_modules
COPY --chown=${USER}:${USER} --from=build $APP_PATH/build ./build
COPY --chown=${USER}:${USER} --from=build $APP_PATH/server ./server
COPY --chown=${USER}:${USER} --from=build $APP_PATH/public ./public
COPY --chown=${USER}:${USER} --from=build $APP_PATH/.sequelizerc .
COPY --chown=${USER}:${USER} --from=build $APP_PATH/package.json .
ENV NODE_ENV=production
COPY --from=build $APP_PATH/build ./build
COPY --from=build $APP_PATH/server ./server
COPY --from=build $APP_PATH/public ./public
COPY --from=build $APP_PATH/.sequelizerc ./.sequelizerc
COPY --from=build $APP_PATH/node_modules ./node_modules
COPY --from=build $APP_PATH/package.json ./package.json
RUN apt-get update && \
apt-get install -y wget && \
rm -rf /var/lib/apt/lists/*
RUN addgroup --gid 1001 nodejs && \
adduser --uid 1001 --ingroup nodejs nodejs && \
chown -R nodejs:nodejs $APP_PATH/build && \
mkdir -p /var/lib/outline && \
chown -R nodejs:nodejs /var/lib/outline
ENV FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
RUN mkdir -p "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chown -R nodejs:nodejs "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chmod 1777 "$FILE_STORAGE_LOCAL_ROOT_DIR"
VOLUME /var/lib/outline/data
USER nodejs
HEALTHCHECK CMD wget -qO- http://localhost:${PORT}/_health | grep -q "OK" || exit 1
EXPOSE 3000
CMD ["yarn", "start"]
ENV PORT=3000
USER ${USER}
EXPOSE ${PORT}
VOLUME ${DATA_PATH}
HEALTHCHECK --interval=1m CMD curl -fs localhost:${PORT}/_health | grep -q OK || exit 1
CMD ["node", "build/server/index.js"]
+24
View File
@@ -0,0 +1,24 @@
ARG APP_PATH=/opt/outline
ARG SRC_PATH=./outline
FROM node:24.15.0
ARG APP_PATH
ARG SRC_PATH
ARG CDN_URL
WORKDIR $APP_PATH
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ${SRC_PATH}/.yarnrc.yml ./
COPY ${SRC_PATH}/patches ./patches
ENV NODE_OPTIONS='--max-old-space-size=24000'
RUN corepack enable && \
yarn install --immutable --network-timeout 1000000 && \
yarn cache clean
COPY ${SRC_PATH} .
COPY ./patches/* .
RUN for patch in $(ls *.patch); do patch -p1 < $patch; done
RUN cat << EOF > /entrypoint.sh
yarn dev:watch
EOF
ENV DATA_PATH=/var/lib/outline/data
VOLUME ${DATA_PATH}
STOPSIGNAL SIGKILL
ENTRYPOINT ["bash", "/entrypoint.sh"]
+19 -97
View File
@@ -1,103 +1,25 @@
Business Source License 1.1
MIT License
Parameters
Copyright (c) 2025 flameshikari
Licensor: General Outline, Inc.
Licensed Work: Outline 0.82.0-0
The Licensed Work is (c) 2025 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
Service.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
A “Document Service” is a commercial offering that
allows third parties (other than your employees and
contractors) to access the functionality of the
Licensed Work by creating teams and documents
controlled by such third parties.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Change Date: 2029-01-31
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Change License: Apache License, Version 2.0
---
For information about alternative licensing arrangements for the Software,
please visit: https://www.getoutline.com
Notice
The Business Source License (this document, or the “License”) is not an Open
Source license. However, the Licensed Work will eventually be made available
under an Open Source License, as stated in this License.
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
“Business Source License” is a trademark of MariaDB Corporation Ab.
-----------------------------------------------------------------------------
Business Source License 1.1
Terms
The Licensor hereby grants you the right to copy, modify, create derivative
works, redistribute, and make non-production use of the Licensed Work. The
Licensor may make an Additional Use Grant, above, permitting limited
production use.
Effective on the Change Date, or the fourth anniversary of the first publicly
available distribution of a specific version of the Licensed Work under this
License, whichever comes first, the Licensor hereby grants you rights under
the terms of the Change License, and the rights granted in the paragraph
above terminate.
If your use of the Licensed Work does not comply with the requirements
currently in effect as described in this License, you must purchase a
commercial license from the Licensor, its affiliated entities, or authorized
resellers, or you must refrain from using the Licensed Work.
All copies of the original and modified Licensed Work, and derivative works
of the Licensed Work, are subject to this License. This License applies
separately for each version of the Licensed Work and the Change Date may vary
for each version of the Licensed Work released by Licensor.
You must conspicuously display this License on each original or modified copy
of the Licensed Work. If you receive the Licensed Work in original or
modified form from a third party, the terms and conditions set forth in this
License apply to your use of that work.
Any use of the Licensed Work in violation of this License will automatically
terminate your rights under this License for the current and all other
versions of the Licensed Work.
This License does not grant you any right in any trademark or logo of
Licensor or its affiliates (provided that you may use a trademark or logo of
Licensor as expressly required by this License).
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
TITLE.
MariaDB hereby grants you permission to use this Licenses text to license
your works, and to refer to it using the trademark “Business Source License”,
as long as you comply with the Covenants of Licensor below.
Covenants of Licensor
In consideration of the right to use this Licenses text and the “Business
Source License” name and trademark, Licensor covenants to MariaDB, and to all
other recipients of the licensed work to be provided by Licensor:
1. To specify as the Change License the GPL Version 2.0 or any later version,
or a license that is compatible with GPL Version 2.0 or a later version,
where “compatible” means that software provided under the Change License can
be included in a program with software provided under GPL Version 2.0 or a
later version. Licensor may specify additional Change Licenses without
limitation.
2. To either: (a) specify an additional grant of rights to use that does not
impose any additional restriction on the right granted in this License, as
the Additional Use Grant; or (b) insert the text “None”.
3. To specify a Change Date.
4. Not to modify this License in any other way.
This repository includes Outline Wiki as a submodule, which is licensed under the Business Source License 1.1 (BSL). See the outline directory for its specific license terms.
+85 -18
View File
@@ -1,5 +1,3 @@
![](.github/assets/opengraph.png)
# 📚 [Outline](https://github.com/outline/outline) с русским переводом [![Build Status](https://img.shields.io/github/actions/workflow/status/flameshikari/outline-ru/build.yml)](https://github.com/flameshikari/outline-ru/actions) [![Version](https://img.shields.io/github/v/release/flameshikari/outline-ru?style=)](https://github.com/flameshikari/outline-ru/releases/latest)
## ❓ Зачем
@@ -8,30 +6,25 @@
## 📝 Примечания
За основу взят перевод из [данного коммита](https://github.com/outline/outline/commit/228d1faa9fd3cbb82409d98e1443fed65adc5715), который впоследствии улучшается и переводится здесь.
Буду рад помощи в улучшении перевода или сборки; сообщить о некорректном переводе можно [здесь](https://github.com/flameshikari/outline-ru/discussions/8).
Из доступных архитектур контейнера имеются только `amd64` и `arm64`.
## ⚠️ Дисклеймер
Данное программное обеспечение предоставляется «как есть», без каких-либо гарантий, явно выраженных или подразумеваемых, включая гарантии товарной пригодности, соответствия по его конкретному назначению и отсутствия нарушений, но не ограничиваясь ими. Ни в каком случае авторы или правообладатели не несут ответственности по каким-либо искам, за ущерб или по иным требованиям, в том числе, при действии контракта, деликте или иной ситуации, возникшим из-за использования программного обеспечения или иных действий с программным обеспечением.
- образ доступен в [Docker Hub](https://hub.docker.com/r/flameshikari/outline-ru/tags) и [GHCR](https://github.com/flameshikari/outline-ru/pkgs/container/outline-ru)
- за основу взят перевод из [этого коммита](https://github.com/outline/outline/commit/228d1faa9fd3cbb82409d98e1443fed65adc5715)
- сообщить о некорректном переводе можно [тут](https://github.com/flameshikari/outline-ru/discussions/8)
## 🐳 Установка
> Перед установкой **ОБЯЗАТЕЛЬНО** прочтите [про бэкапы перед обновлением](https://docs.getoutline.com/s/hosting/doc/backups-KZtPOADCHG).
> [!WARNING]
> Перед обновлением **ОБЯЗАТЕЛЬНО** делайте [бэкап](https://docs.getoutline.com/s/hosting/doc/backups-KZtPOADCHG)!
Всё делается по [официальной документации](https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t), только в качестве `image` укажите `flameshikari/outline-ru:latest` или `ghcr.io/flameshikari/outline-ru:latest` (вместо `latest` желательно указать версию; доступные смотреть [здесь](https://github.com/flameshikari/outline-ru/tags)), а также задайте переменную `DEFAULT_LANGUAGE=ru_RU` в [docker.env](https://github.com/outline/outline/blob/main/.env.sample) или в `environments` (в зависимости от вашей конфигурации).
Следуйте [официальной инструкции](https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t), только в качестве `image` укажите `flameshikari/outline-ru:latest` (желательно зафиксировать версию, заменив `latest` на один из [доступных тегов](https://github.com/flameshikari/outline-ru/tags)). Например:
```yaml
...
services:
outline:
image: flameshikari/outline-ru:latest
image: flameshikari/outline-ru:1.8.1
# image: ghcr.io/flameshikari/outline-ru:1.8.1
env_file: ./docker.env
ports:
- "3000:3000"
expose:
- 3000
volumes:
- storage-data:/var/lib/outline/data
depends_on:
@@ -40,3 +33,77 @@
...
```
## 🛠️ Разработка
### Ключевые файлы
- русский перевод — [translation/ru.json](./translation/ru.json)
- английский перевод — [outline/shared/i18n/locales/en_US/translation.json](https://github.com/outline/outline/blob/main/shared/i18n/locales/en_US/translation.json)
- временный файл — [translation/tmp.json]() (существует только локально)
### Описание работы скрипта
Скрипт [translation/merge.py](./translation/merge.py) используется для объединения английского и русского переводов во временный файл. Скрипт не имеет интерактивного режима и каких-либо аргументов/опций, он просто запускается (с выводом некоторой полезной информации) и делает следующее:
- сохраняет актуальные переведённые строки
- удаляет неактуальные переведённые строки
- если в русском переводе есть одинаковые key/value пары, то они считаются исключениями (например, `HTML` или `API`) и переносятся как есть
- новые непереведённые строки добавляются в конец
> Также скрипт конвертирует строки с суффиксом `_plural` (англ. множественное число) в строки с числовыми суффиксами, поддерживающие склонение по падежам. Например, имеем исходные данные:
>
> ```jsonc
> {
> // ...
> "{{ count }} comment": "{{ count }} comment",
> "{{ count }} comment_plural": "{{ count }} comments"
> // ...
> }
> ```
> Строки после конвертации скриптом:
> ```jsonc
> {
> // ...
> "{{ count }} comment_0": "[NOT TRANSLATED]",
> "{{ count }} comment_1": "[NOT TRANSLATED]",
> "{{ count }} comment_2": "[NOT TRANSLATED]"
> // ...
> }
> ```
> Конвертированные строки после перевода:
> ```jsonc
> {
> // ...
> "{{ count }} comment_0": "{{ count }} комментарий", // ед. число, им. падеж
> "{{ count }} comment_1": "{{ count }} комментария", // ед. число, род. падеж
> "{{ count }} comment_2": "{{ count }} комментариев" // мн. число, род. падеж
> // ...
> }
> ```
> Документация: [множественное число](https://www.i18next.com/translation-function/plurals#languages-with-multiple-plurals) в [i18next](https://www.i18next.com) с [JSON-форматом версии 3](https://www.i18next.com/misc/json-format#i18next-json-v3)
### Быстрый старт
1. Клонирование репозитория с подмодулем:
```sh
git clone --recurse-submodules git@github.com:flameshikari/outline-ru.git
```
2. Пулл изменений в подмодуле и переключение на последний доступный тег:
```sh
git submodule foreach 'git pull --rebase --tags && git checkout v1.8.1'
```
3. Запуск контейнеров:
```sh
docker compose up -d --build
```
Веб-интерфейс Outline будет доступен по [этой ссылке](http://localhost:10240); входить с помощью OpenID Connect под логином/паролем `outline`.
4. Формирование временного файла с помощью [translation/merge.py](./translation/merge.py):
```sh
python translation/merge.py
```
После можно приступить к переводу сфомированного временного файла. После перевода временного файла скопируйте его в файл русского перевода. Любые изменения в русском переводе обновят [открытую веб-страницу](http://localhost:10240) через пару секунд.
+73
View File
@@ -0,0 +1,73 @@
volumes:
outline:
name: outline
outline-postgres:
name: outline-postgres
networks:
default:
name: outline
x-outline-base: &outline-base
container_name: outline
depends_on:
- outline-services
environment:
FILE_STORAGE: local
FORCE_HTTPS: false
PORT: ${PORT:-10240}
PORT_VITE: ${PORT_VITE:-10242}
URL: http://127.0.0.1:${PORT:-10240}
SECRET_KEY: ${SECRET:-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef}
UTILS_SECRET: ${SECRET:-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef}
REDIS_URL: redis://outline-services
DATABASE_URL: postgres://outline:outline@outline-services/outline
PGSSLMODE: disable
OIDC_ISSUER_URL: http://outline-services:8080
OIDC_CLIENT_ID: outline
OIDC_CLIENT_SECRET: outline
OIDC_DISPLAY_NAME: OIDC
services:
outline:
<<: *outline-base
profiles: [prod]
build: .
image: flameshikari/outline-ru:nightly
ports:
- ${PORT:-10240}:${PORT:-10240}
pull_policy: always
volumes:
- outline:/var/lib/outline/data
outline-dev:
<<: *outline-base
build:
dockerfile: Dockerfile.dev
profiles: [dev]
ports:
- ${PORT:-10240}:${PORT:-10240}
- ${PORT_VITE:-10242}:${PORT_VITE:-10242}
volumes:
- outline:/var/lib/outline/data
- ./translation/ru.json:/opt/outline/shared/i18n/locales/ru_RU/translation.json
outline-services:
container_name: outline-services
image: outline-services
build: services
environment:
ISSUER: http://outline-services:8080
PUBLIC_URL: http://127.0.0.1:${PORT_OIDC:-10241}
volumes:
- outline-postgres:/var/lib/postgresql/data
ports:
- ${PORT_OIDC:-10241}:8080
healthcheck:
test: |
pg_isready -U outline && \
redis-cli ping && \
wget -qO- http://127.0.0.1:8080/health
interval: 10s
timeout: 5s
retries: 5
Submodule
+1
Submodule outline added at c2edd41e87
+122
View File
@@ -0,0 +1,122 @@
diff --git a/app/utils/i18n.ts b/app/utils/i18n.ts
index d20fe802a..de4987a99 100644
--- a/app/utils/i18n.ts
+++ b/app/utils/i18n.ts
@@ -51,5 +51,16 @@ export function initI18n(defaultLanguage = "en_US") {
Logger.error("Failed to initialize i18n", err);
});
+ // HMR: when a translation JSON changes on disk, the Vite dev server emits
+ // an "i18n:update" event (see vite.config.ts). Reload the resources for
+ // that language and ask react-i18next to re-render the tree.
+ if (typeof import.meta.hot !== "undefined") {
+ import.meta.hot.on("i18n:update", async ({ lng: changed }) => {
+ const target = unicodeCLDRtoBCP47(changed);
+ await i18n.reloadResources(target);
+ i18n.emit("languageChanged", i18n.language);
+ });
+ }
+
return i18n;
}
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts
index 477793da6..93bc7c6e9 100644
--- a/server/middlewares/csp.ts
+++ b/server/middlewares/csp.ts
@@ -59,8 +59,9 @@ export default function createCSPMiddleware(options?: CSPOptions) {
// Allow to load assets from Vite
if (!env.isProduction) {
- scriptSrc.push(env.URL.replace(`:${env.PORT}`, ":3001"));
- scriptSrc.push("localhost:3001");
+ const vitePort = Number(process.env.PORT_VITE) || 3001;
+ scriptSrc.push(env.URL.replace(`:${env.PORT}`, `:${vitePort}`));
+ scriptSrc.push(`localhost:${vitePort}`);
} else {
scriptSrc.push(env.URL);
}
diff --git a/server/routes/app.ts b/server/routes/app.ts
index b598752f2..11667c560 100644
--- a/server/routes/app.ts
+++ b/server/routes/app.ts
@@ -22,7 +22,8 @@ import { loadPublicShare } from "@server/commands/shareLoader";
const readFile = util.promisify(fs.readFile);
const entry = "app/index.tsx";
-const viteHost = env.URL.replace(`:${env.PORT}`, ":3001");
+const vitePort = Number(process.env.PORT_VITE) || 3001;
+const viteHost = env.URL.replace(`:${env.PORT}`, `:${vitePort}`);
let indexHtmlCache: Buffer | undefined;
diff --git a/server/routes/index.ts b/server/routes/index.ts
index 0d304ec50..e32ec2da8 100644
--- a/server/routes/index.ts
+++ b/server/routes/index.ts
@@ -101,14 +101,23 @@ router.get("/locales/:lng.json", async (ctx) => {
await send(ctx, path.join(lng, "translation.json"), {
setHeaders: (res, _, stats) => {
res.setHeader("Last-Modified", formatRFC7231(stats.mtime));
- res.setHeader("Cache-Control", `public, max-age=${7 * Day.seconds}`);
+ res.setHeader(
+ "Cache-Control",
+ env.isDevelopment
+ ? "no-store"
+ : `public, max-age=${7 * Day.seconds}`
+ );
res.setHeader(
"ETag",
crypto.createHash("md5").update(stats.mtime.toISOString()).digest("hex")
);
res.setHeader("Access-Control-Allow-Origin", "*");
},
- root: path.join(__dirname, "../../shared/i18n/locales"),
+ // In dev read the source tree directly so the bind-mounted translation
+ // files (and HMR'd changes) are seen without rebuilding.
+ root: env.isDevelopment
+ ? path.join(__dirname, "../../../shared/i18n/locales")
+ : path.join(__dirname, "../../shared/i18n/locales"),
});
});
diff --git a/vite.config.ts b/vite.config.ts
index 829346243..244308bb3 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -31,7 +31,7 @@ export default () =>
publicDir: "./server/static",
base: (environment.CDN_URL ?? "") + "/static/",
server: {
- port: 3001,
+ port: Number(process.env.PORT_VITE),
host: true,
https: httpsConfig,
allowedHosts: host ? [host] : undefined,
@@ -45,6 +45,27 @@ export default () =>
: { strict: true },
},
plugins: [
+ {
+ // Custom HMR for translation JSON files. Watches shared/i18n/locales
+ // and sends a custom WS event so the client can call
+ // i18n.reloadResources() instead of doing a full page reload.
+ name: "outline-i18n-hmr",
+ apply: "serve",
+ configureServer(server) {
+ const dir = path.resolve(__dirname, "shared/i18n/locales");
+ server.watcher.add(dir);
+ server.watcher.on("change", (file) => {
+ const m = /locales[\\/]([^\\/]+)[\\/]translation\.json$/.exec(file);
+ if (m) {
+ server.ws.send({
+ type: "custom",
+ event: "i18n:update",
+ data: { lng: m[1] },
+ });
+ }
+ });
+ },
+ },
react(),
// https://vite-pwa-org.netlify.app/
VitePWA({
+74
View File
@@ -0,0 +1,74 @@
diff --git a/app/components/Sidebar/components/Version.tsx b/app/components/Sidebar/components/Version.tsx
index f2e8810b2..d4e202447 100644
--- a/app/components/Sidebar/components/Version.tsx
+++ b/app/components/Sidebar/components/Version.tsx
@@ -30,7 +30,7 @@ export default function Version() {
return (
<SidebarLink
target="_blank"
- href="https://github.com/outline/outline/releases"
+ href="https://github.com/flameshikari/outline-ru/releases"
label={
<>
v{currentVersion}
diff --git a/server/env.ts b/server/env.ts
index 34f926029..88f97d515 100644
--- a/server/env.ts
+++ b/server/env.ts
@@ -245,7 +245,7 @@ export class Environment {
*/
@Public
@IsIn(languages)
- public DEFAULT_LANGUAGE = environment.DEFAULT_LANGUAGE ?? "en_US";
+ public DEFAULT_LANGUAGE = environment.DEFAULT_LANGUAGE ?? "ru_RU";
/**
* A comma list of which services should be enabled on this instance defaults to all.
diff --git a/server/utils/getInstallationInfo.ts b/server/utils/getInstallationInfo.ts
index 1d11a426c..00ec42f69 100644
--- a/server/utils/getInstallationInfo.ts
+++ b/server/utils/getInstallationInfo.ts
@@ -2,7 +2,7 @@ import { version } from "../../package.json";
import fetch from "./fetch";
const dockerhubLink =
- "https://hub.docker.com/v2/repositories/outlinewiki/outline";
+ "https://hub.docker.com/v2/repositories/flameshikari/outline-ru";
function isFullReleaseVersion(versionName: string): boolean {
const releaseRegex = /^(version-)?\d+\.\d+\.\d+$/; // Matches "N.N.N" or "version-N.N.N" for dockerhub releases before v0.56.0"
diff --git a/shared/i18n/index.ts b/shared/i18n/index.ts
index e315ef413..b580ec795 100644
--- a/shared/i18n/index.ts
+++ b/shared/i18n/index.ts
@@ -72,6 +72,10 @@ export const languageOptions: LanguageOption[] = [
label: "فارسی (Persian)",
value: "fa_IR",
},
+ {
+ label: "Русский (Russian)",
+ value: "ru_RU",
+ },
{
label: "Svenska (Swedish)",
value: "sv_SE",
diff --git a/shared/utils/date.ts b/shared/utils/date.ts
index 397b2c7a4..a45d418ae 100644
--- a/shared/utils/date.ts
+++ b/shared/utils/date.ts
@@ -23,6 +23,7 @@ import {
ptBR,
pt,
pl,
+ ru,
sv,
tr,
vi,
@@ -175,6 +176,7 @@ const locales = {
pt_BR: ptBR,
pt_PT: pt,
pl_PL: pl,
+ ru_RU: ru,
sv_SE: sv,
tr_TR: tr,
uk_UA: uk,
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env bash
CWD="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
cat $CWD/src/shared/utils/date.ts | \
sed '/^ pl,/a\ \ ru,' | \
sed '/^ pl_PL: pl,/a\ \ ru_RU: ru,' \
> $CWD/shared/utils/date.ts
cat $CWD/src/shared/i18n/index.ts | \
sed '/^export const languageOptions: LanguageOption\[\] = \[/a\ \ {\n label: "Русский (Russian)",\n value: "ru_RU",\n },' \
> $CWD/shared/i18n/index.ts
+17
View File
@@ -0,0 +1,17 @@
FROM oven/bun:1-alpine
USER root
RUN apk add --no-cache postgresql17 postgresql17-contrib redis su-exec bash
ENV PGDATA=/var/lib/postgresql/data
RUN mkdir -p "$PGDATA" /run/postgresql /var/log \
&& chown -R postgres:postgres "$PGDATA" /run/postgresql
WORKDIR /app
COPY package.json ./
RUN bun install --production
COPY server.js entrypoint.sh avatar.png ./
RUN chmod +x entrypoint.sh
CMD ["./entrypoint.sh"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+40
View File
@@ -0,0 +1,40 @@
#!/bin/sh
set -e
PG_USER="${POSTGRES_USER:-${COMMON:-outline}}"
PG_PASS="${POSTGRES_PASSWORD:-${COMMON:-outline}}"
PG_DB="${POSTGRES_DB:-${COMMON:-outline}}"
# named-volume mount comes up root-owned; reclaim it for postgres
chown -R postgres:postgres "$PGDATA" /run/postgresql
chmod 700 "$PGDATA"
# ---- Postgres ----
if [ ! -s "$PGDATA/PG_VERSION" ]; then
echo "==> initdb in $PGDATA"
su-exec postgres initdb -D "$PGDATA" --username="$PG_USER" \
--auth-local=trust --auth-host=md5 >/dev/null
echo "listen_addresses = '*'" >> "$PGDATA/postgresql.conf"
echo "host all all 0.0.0.0/0 md5" >> "$PGDATA/pg_hba.conf"
fi
echo "==> starting postgres on :5432"
su-exec postgres pg_ctl -D "$PGDATA" -l "$PGDATA/postgres.log" -w start
# ensure password is set (md5 auth needs it for host connections)
su-exec postgres psql -U "$PG_USER" -d postgres -c \
"ALTER ROLE \"$PG_USER\" WITH LOGIN PASSWORD '$PG_PASS';" >/dev/null
# create db idempotently
su-exec postgres psql -U "$PG_USER" -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname='$PG_DB'" | grep -q 1 || \
su-exec postgres createdb -U "$PG_USER" -O "$PG_USER" "$PG_DB"
# ---- Redis ----
echo "==> starting redis on :6379"
redis-server --daemonize yes --bind 0.0.0.0 --port 6379 \
--logfile "" --protected-mode no
# ---- OIDC mock (foreground) ----
echo "==> starting bun OIDC mock on :${PORT_OIDC:-8080}"
exec bun server.js
+13
View File
@@ -0,0 +1,13 @@
{
"name": "mock-oidc",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "bun server.js"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.0",
"jose": "^5.9.6"
}
}
+230
View File
@@ -0,0 +1,230 @@
// Mock OIDC server with autologin to a single user.
// Compatible with IdentityServer-style clients (/connect/*) and generic OIDC clients.
// NOT FOR PRODUCTION.
import express from 'express';
import cors from 'cors';
import crypto from 'crypto';
import { SignJWT, exportJWK, generateKeyPair } from 'jose';
// ---------- Config ----------
// ISSUER = internal URL used by server-side relying parties (outline backend),
// also signed into JWT `iss` claim.
// PUBLIC = browser-facing URL (host:published-port) used in redirect endpoints.
// PORT = derived from ISSUER URL so a single var sets where we listen.
const ISSUER = process.env.ISSUER || 'http://localhost:8080';
const PUBLIC = process.env.PUBLIC_URL || ISSUER;
const PORT = parseInt(new URL(ISSUER).port || '8080', 10);
const CLIENT_ID = process.env.CLIENT_ID || 'mock-client';
const CLIENT_SECRET = process.env.CLIENT_SECRET || 'mock-secret';
const TOKEN_TTL = parseInt(process.env.TOKEN_TTL || '3600', 10);
const CODE_TTL = parseInt(process.env.CODE_TTL || '60', 10);
const USER = {
sub: process.env.USER_SUB || 'user-1',
email: process.env.USER_EMAIL || 'mail@example.com',
email_verified: true,
name: process.env.USER_NAME || 'Outline',
preferred_username: process.env.USER_USERNAME || 'outline',
given_name: process.env.USER_GIVEN || 'Outline',
family_name: process.env.USER_FAMILY || 'Wiki',
roles: (process.env.USER_ROLES || 'admin,user').split(','),
picture: `${PUBLIC}/avatar.png`,
};
// ---------- Keys ----------
const { publicKey, privateKey } = await generateKeyPair('RS256');
const jwk = { ...(await exportJWK(publicKey)), kid: 'mock-key-1', alg: 'RS256', use: 'sig' };
// ---------- In-memory stores ----------
const codes = new Map();
const tokens = new Map();
const refreshTokens = new Map();
// ---------- Helpers ----------
const now = () => Math.floor(Date.now() / 1000);
const signJwt = (payload, audience, ttl = TOKEN_TTL) =>
new SignJWT(payload)
.setProtectedHeader({ alg: 'RS256', kid: jwk.kid, typ: 'JWT' })
.setIssuer(ISSUER)
.setSubject(USER.sub)
.setAudience(audience)
.setIssuedAt(now())
.setExpirationTime(now() + ttl)
.setJti(crypto.randomBytes(16).toString('hex'))
.sign(privateKey);
const extractClientCreds = (req) => {
const auth = req.headers.authorization;
if (auth?.startsWith('Basic ')) {
const decoded = Buffer.from(auth.slice(6), 'base64').toString();
const idx = decoded.indexOf(':');
return {
clientId: decodeURIComponent(decoded.slice(0, idx)),
clientSecret: decodeURIComponent(decoded.slice(idx + 1)),
};
}
return { clientId: req.body.client_id, clientSecret: req.body.client_secret };
};
const pkceMatches = (verifier, challenge, method) => {
if (!verifier) return false;
if (method === 'S256') return crypto.createHash('sha256').update(verifier).digest('base64url') === challenge;
return verifier === challenge;
};
// ---------- App ----------
const app = express();
app.use(cors({ origin: true, credentials: true }));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use((req, _res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// ---------- Discovery + JWKS ----------
app.get('/.well-known/openid-configuration', (_req, res) => res.json({
issuer: ISSUER,
authorization_endpoint: `${PUBLIC}/connect/authorize`,
token_endpoint: `${ISSUER}/connect/token`,
userinfo_endpoint: `${ISSUER}/connect/userinfo`,
end_session_endpoint: `${PUBLIC}/connect/endsession`,
jwks_uri: `${ISSUER}/.well-known/jwks`,
response_types_supported: ['code'],
response_modes_supported: ['query', 'fragment'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
scopes_supported: ['openid', 'profile', 'email', 'offline_access'],
token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic', 'none'],
claims_supported: ['sub', 'email', 'email_verified', 'name', 'preferred_username', 'given_name', 'family_name', 'roles'],
code_challenge_methods_supported: ['S256', 'plain'],
grant_types_supported: ['authorization_code', 'refresh_token'],
}));
app.get(['/.well-known/jwks', '/.well-known/openid-configuration/jwks', '/jwks'],
(_req, res) => res.json({ keys: [jwk] }));
// ---------- /authorize: AUTOLOGIN ----------
app.get(['/connect/authorize', '/authorize'], (req, res) => {
const { redirect_uri, state, nonce, scope, response_type,
client_id, code_challenge, code_challenge_method } = req.query;
const fail = (status, error, description) =>
res.status(status).json({ error, error_description: description });
if (response_type !== 'code') return fail(400, 'unsupported_response_type', 'response_type must be "code"');
if (!client_id) return fail(400, 'invalid_request', 'client_id is required');
if (!redirect_uri) return fail(400, 'invalid_request', 'redirect_uri is required');
const code = crypto.randomBytes(32).toString('hex');
codes.set(code, {
client_id, redirect_uri, nonce,
scope: scope || 'openid',
code_challenge, code_challenge_method,
expires_at: Date.now() + CODE_TTL * 1000,
});
const url = new URL(redirect_uri);
url.searchParams.set('code', code);
if (state) url.searchParams.set('state', state);
console.log(` -> autologin: redirect to ${url.toString()}`);
res.redirect(url.toString());
});
// ---------- /token ----------
app.post(['/connect/token', '/token'], async (req, res) => {
const { grant_type } = req.body;
if (grant_type === 'authorization_code') {
const { code, redirect_uri, code_verifier } = req.body;
const { clientId, clientSecret } = extractClientCreds(req);
const ctx = codes.get(code);
codes.delete(code);
if (!ctx) return res.status(400).json({ error: 'invalid_grant', error_description: 'unknown code' });
if (ctx.expires_at < Date.now()) return res.status(400).json({ error: 'invalid_grant', error_description: 'code expired' });
if (ctx.redirect_uri !== redirect_uri) return res.status(400).json({ error: 'invalid_grant', error_description: 'redirect_uri mismatch' });
if (ctx.code_challenge) {
if (!pkceMatches(code_verifier, ctx.code_challenge, ctx.code_challenge_method))
return res.status(400).json({ error: 'invalid_grant', error_description: 'PKCE verification failed' });
} else if (clientId !== CLIENT_ID || clientSecret !== CLIENT_SECRET) {
return res.status(401).json({ error: 'invalid_client' });
}
const aud = clientId || CLIENT_ID;
const id_token = await signJwt({ ...USER, nonce: ctx.nonce, auth_time: now() }, aud);
const access_token = await signJwt({ scope: ctx.scope, client_id: aud, ...USER }, aud);
tokens.set(access_token, USER);
const response = { access_token, id_token, token_type: 'Bearer', expires_in: TOKEN_TTL, scope: ctx.scope };
if (ctx.scope.split(' ').includes('offline_access')) {
const refresh_token = crypto.randomBytes(32).toString('hex');
refreshTokens.set(refresh_token, { client_id: aud, scope: ctx.scope });
response.refresh_token = refresh_token;
}
return res.json(response);
}
if (grant_type === 'refresh_token') {
const { refresh_token } = req.body;
const ctx = refreshTokens.get(refresh_token);
if (!ctx) return res.status(400).json({ error: 'invalid_grant' });
const id_token = await signJwt({ ...USER, auth_time: now() }, ctx.client_id);
const access_token = await signJwt({ scope: ctx.scope, client_id: ctx.client_id, ...USER }, ctx.client_id);
tokens.set(access_token, USER);
return res.json({
access_token, id_token, refresh_token,
token_type: 'Bearer', expires_in: TOKEN_TTL, scope: ctx.scope,
});
}
res.status(400).json({ error: 'unsupported_grant_type' });
});
// ---------- /userinfo ----------
app.all(['/connect/userinfo', '/userinfo'], (req, res) => {
const auth = req.headers.authorization;
const user = auth?.startsWith('Bearer ') ? tokens.get(auth.slice(7)) : null;
if (!user) return res.status(401).json({ error: 'invalid_token' });
res.json(user);
});
// ---------- /endsession ----------
app.get(['/connect/endsession', '/logout'], (req, res) => {
const { post_logout_redirect_uri, state } = req.query;
if (post_logout_redirect_uri) {
const url = new URL(post_logout_redirect_uri);
if (state) url.searchParams.set('state', state);
return res.redirect(url.toString());
}
res.type('html').send('<h1>Logged out</h1>');
});
// ---------- Index + health + 404 ----------
app.get('/', (_req, res) => res.type('html').send(`<h1>OIDC Mock Server</h1>
<p>Autologin as <strong>${USER.email}</strong> (sub: <code>${USER.sub}</code>)</p>
<ul>
<li><a href="/.well-known/openid-configuration">/.well-known/openid-configuration</a></li>
<li><a href="/.well-known/jwks">/.well-known/jwks</a></li>
<li><a href="/connect/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=http://localhost:3000/callback&scope=openid+profile+email&state=test">Simulate /authorize</a></li>
</ul>
<p>Client ID: <code>${CLIENT_ID}</code> &middot; Client Secret: <code>${CLIENT_SECRET}</code></p>`));
app.get('/avatar.png', (_req, res) => res.sendFile('avatar.png', { root: import.meta.dirname }));
app.get('/health', (_req, res) => res.json({ status: 'ok', issuer: ISSUER }));
app.use((req, res) => res.status(404).json({
error: 'not_found', path: req.path,
hint: 'See /.well-known/openid-configuration',
}));
// ---------- Start ----------
app.listen(PORT, () => {
console.log(`Mock OIDC listening on :${PORT} issuer=${ISSUER} public=${PUBLIC} client_id=${CLIENT_ID} autologin=${USER.email} (sub=${USER.sub}, roles=${USER.roles.join(',')})`);
});
-97
View File
@@ -1,97 +0,0 @@
import { locales } from "../utils/date";
type LanguageOption = {
label: string;
value: keyof typeof locales;
};
// Note: Updating the available languages? Make sure to also update the
// locales array in shared/utils/date.ts to enable translation for timestamps.
export const languageOptions: LanguageOption[] = [
{
label: "Русский (Russian)",
value: "ru_RU",
},
{
label: "English (US)",
value: "en_US",
},
{
label: "Čeština (Czech)",
value: "cs_CZ",
},
{
label: "简体中文 (Chinese, Simplified)",
value: "zh_CN",
},
{
label: "繁體中文 (Chinese, Traditional)",
value: "zh_TW",
},
{
label: "Deutsch (German)",
value: "de_DE",
},
{
label: "Español (Spanish)",
value: "es_ES",
},
{
label: "Français (French)",
value: "fr_FR",
},
{
label: "Italiano (Italian)",
value: "it_IT",
},
{
label: "日本語 (Japanese)",
value: "ja_JP",
},
{
label: "한국어 (Korean)",
value: "ko_KR",
},
{
label: "Nederland (Dutch, Netherlands)",
value: "nl_NL",
},
{
label: "Norsk Bokmål (Norwegian)",
value: "nb_NO",
},
{
label: "Português (Portuguese, Brazil)",
value: "pt_BR",
},
{
label: "Português (Portuguese, Portugal)",
value: "pt_PT",
},
{
label: "Polskie (Polish)",
value: "pl_PL",
},
{
label: "فارسی (Persian)",
value: "fa_IR",
},
{
label: "Svenska (Swedish)",
value: "sv_SE",
},
{
label: "Türkçe (Turkish)",
value: "tr_TR",
},
{
label: "Українська (Ukrainian)",
value: "uk_UA",
},
{
label: "Tiếng Việt (Vietnamese)",
value: "vi_VN",
},
];
export const languages = languageOptions.map((i) => i.value);
File diff suppressed because it is too large Load Diff
-198
View File
@@ -1,198 +0,0 @@
/* eslint-disable import/no-duplicates */
import {
Locale,
addSeconds,
formatDistanceToNow,
subDays,
subMonths,
subWeeks,
subYears,
} from "date-fns";
import {
cs,
de,
enUS,
es,
faIR,
fr,
it,
ja,
ko,
nb,
nl,
ptBR,
pt,
pl,
ru,
sv,
tr,
vi,
uk,
zhCN,
zhTW,
} from "date-fns/locale";
import type { DateFilter } from "../types";
export function subtractDate(date: Date, period: DateFilter) {
switch (period) {
case "day":
return subDays(date, 1);
case "week":
return subWeeks(date, 1);
case "month":
return subMonths(date, 1);
case "year":
return subYears(date, 1);
default:
return date;
}
}
/**
* Returns a humanized relative time string for the given date.
*
* @param date The date to convert
* @param options The options to pass to date-fns
* @returns The relative time string
*/
export function dateToRelative(
date: Date | number,
options?: {
includeSeconds?: boolean;
addSuffix?: boolean;
locale?: Locale | undefined;
shorten?: boolean;
}
) {
const now = new Date();
const parsedDateTime = new Date(date);
// Protect against "in less than a minute" when users computer clock is off.
const normalizedDateTime =
parsedDateTime > now && parsedDateTime < addSeconds(now, 60)
? now
: parsedDateTime;
const output = formatDistanceToNow(normalizedDateTime, options);
// Some tweaks to make english language shorter.
if (options?.shorten) {
return output
.replace("about", "")
.replace("less than a minute ago", "just now")
.replace("minute", "min");
}
return output;
}
/**
* Converts a locale string from Unicode CLDR format to BCP47 format.
*
* @param locale The locale string to convert
* @returns The converted locale string
*/
export function unicodeCLDRtoBCP47(locale: string) {
return locale.replace("_", "-").replace("root", "und");
}
/**
* Converts a locale string from BCP47 format to Unicode CLDR format.
*
* @param locale The locale string to convert
* @returns The converted locale string
*/
export function unicodeBCP47toCLDR(locale: string) {
return locale.replace("-", "_").replace("und", "root");
}
/**
* Converts a locale string from Unicode CLDR format to ISO 639 format.
*
* @param locale The locale string to convert
* @returns The converted locale string
*/
export function unicodeCLDRtoISO639(locale: string) {
return locale.split("_")[0];
}
/**
* Returns the current date as a string formatted depending on current locale.
*
* @returns The current date
*/
export function getCurrentDateAsString(locale?: Intl.LocalesArgument) {
return new Date().toLocaleDateString(locale, {
year: "numeric",
month: "long",
day: "numeric",
});
}
/**
* Returns the current time as a string formatted depending on current locale.
*
* @returns The current time
*/
export function getCurrentTimeAsString(locale?: Intl.LocalesArgument) {
return new Date().toLocaleTimeString(locale, {
hour: "numeric",
minute: "numeric",
});
}
/**
* Returns the current date and time as a string formatted depending on current
* locale.
*
* @returns The current date and time
*/
export function getCurrentDateTimeAsString(locale?: Intl.LocalesArgument) {
return new Date().toLocaleString(locale, {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
});
}
const locales = {
cs_CZ: cs,
de_DE: de,
en_US: enUS,
es_ES: es,
fa_IR: faIR,
fr_FR: fr,
it_IT: it,
ja_JP: ja,
ko_KR: ko,
nb_NO: nb,
nl_NL: nl,
pt_BR: ptBR,
pt_PT: pt,
pl_PL: pl,
ru_RU: ru,
sv_SE: sv,
tr_TR: tr,
uk_UA: uk,
vi_VN: vi,
zh_CN: zhCN,
zh_TW: zhTW,
};
/**
* Returns the date-fns locale object for the given user language preference.
*
* @param language The user language
* @returns The date-fns locale.
*/
export function dateLocale(language: keyof typeof locales | undefined | null) {
return language ? locales[language] : undefined;
}
export { locales };
Submodule src deleted from 7bc687b6bf
@@ -3,19 +3,24 @@
import json
import os
def workdir(path):
def resolve(path):
basepath = os.path.abspath(os.path.dirname(__file__))
abspath = os.path.abspath(basepath + '/' + path)
return abspath
en_json_path = workdir('../src/shared/i18n/locales/en_US/translation.json')
ru_json_path = workdir('../shared/i18n/locales/ru_RU/translation.json')
out_json_path = workdir('./translation.json')
en_json_path = resolve('../outline/shared/i18n/locales/en_US/translation.json')
ru_json_path = resolve('./ru.json')
out_json_name = 'tmp.json'
out_json_path = resolve(out_json_name)
translated_lines = {}
untranslated_lines = {}
exception_lines = {}
placeholder = '[NOT TRANSLATED]'
with open(en_json_path) as target:
en_json = json.load(target)
@@ -23,21 +28,35 @@ with open(ru_json_path) as target:
ru_json = json.load(target)
for key, value in en_json.items():
if key in ru_json.keys():
translated_lines[key] = ru_json[key]
else:
untranslated_lines[key] = en_json[key]
# skip x_plural strings
if key.endswith('_plural'):
continue
# keep translated strings
elif key in ru_json.keys():
translated_lines[key] = ru_json[key]
# process plurals
elif key in en_json.keys() and f'{key}_plural' in en_json.keys():
for i in range(0, 3):
plural = f'{key}_{i}'
if plural in ru_json.keys():
translated_lines[plural] = ru_json[plural]
else:
untranslated_lines[plural] = placeholder
else:
untranslated_lines[key] = placeholder
for key, value in ru_json.items():
if key == value:
exception_lines[key] = value
out_json = {**translated_lines, **untranslated_lines}
if (exception_lines):
print('Потенциально непереведённые строки или исключения:')
print('Исключения:')
print(json.dumps(exception_lines, indent=2, ensure_ascii=False))
print()
@@ -49,4 +68,4 @@ if (untranslated_lines):
with open(out_json_path, 'w') as target:
obj = json.dumps(out_json, indent=2, ensure_ascii=False)
target.write(obj + '\n')
print('Переведенные и непереведенные строки смержены в translation.json')
print(f'Переведенные и непереведенные строки смержены в {out_json_name}')
+1811
View File
File diff suppressed because it is too large Load Diff