├── client
├── .eslintignore
├── __mocks__
│ └── style.js
├── i18n.dir.js
├── bin
│ └── watch
├── images
│ └── logo.png
├── src
│ ├── components
│ │ ├── timeline
│ │ │ ├── revision
│ │ │ │ ├── edit-summary-parts.js
│ │ │ │ ├── timelapse.js
│ │ │ │ ├── title.js
│ │ │ │ ├── byte-change.js
│ │ │ │ ├── revision.container.js
│ │ │ │ └── comment.js
│ │ │ ├── user-list.container.js
│ │ │ ├── status.container.js
│ │ │ ├── timeline.container.js
│ │ │ ├── date-list.container.js
│ │ │ ├── diff
│ │ │ │ ├── diff.container.js
│ │ │ │ ├── header.container.js
│ │ │ │ ├── header-revision.container.js
│ │ │ │ ├── header.js
│ │ │ │ └── header-revision.js
│ │ │ ├── error-message.container.js
│ │ │ ├── user.container.js
│ │ │ ├── date-revisions.container.js
│ │ │ ├── user-list.js
│ │ │ ├── spinner.js
│ │ │ ├── date.js
│ │ │ ├── alert.js
│ │ │ ├── user.js
│ │ │ ├── date-list.js
│ │ │ ├── header.js
│ │ │ ├── revision-list.js
│ │ │ ├── status.js
│ │ │ ├── error-message.js
│ │ │ ├── date-revisions.js
│ │ │ └── timeline.js
│ │ ├── link.js
│ │ ├── fields
│ │ │ ├── select-users.container.js
│ │ │ ├── select-wiki.container.js
│ │ │ ├── date-range.container.js
│ │ │ ├── select-wiki.js
│ │ │ ├── date-picker.js
│ │ │ └── date-range.js
│ │ ├── share
│ │ │ ├── share.container.js
│ │ │ └── share.test.js
│ │ ├── form.js
│ │ └── error-boundary.js
│ ├── entities
│ │ ├── wiki-namespace.js
│ │ ├── page.js
│ │ ├── diff-meta.js
│ │ ├── wiki.js
│ │ ├── revision-meta.js
│ │ ├── diff.js
│ │ ├── revision.js
│ │ └── query.js
│ ├── reducers
│ │ ├── revisions
│ │ │ ├── error.js
│ │ │ ├── index.js
│ │ │ ├── cont.js
│ │ │ ├── status.js
│ │ │ └── list.js
│ │ ├── wikis.js
│ │ ├── index.js
│ │ ├── query.js
│ │ └── diffs.js
│ ├── selectors
│ │ ├── users.js
│ │ ├── date.js
│ │ ├── wiki.js
│ │ ├── side.js
│ │ ├── status.js
│ │ ├── editorinteract.js
│ │ ├── editorinteract.test.js
│ │ ├── diff.js
│ │ └── revisions.js
│ ├── utils
│ │ ├── location-query.js
│ │ ├── intersection.js
│ │ ├── location-query.test.js
│ │ ├── special-wikis-list.js
│ │ └── ip-validator.js
│ ├── actions
│ │ ├── wiki.js
│ │ ├── diff.js
│ │ ├── query.js
│ │ └── revisions.js
│ └── epics
│ │ ├── index.js
│ │ └── query.js
├── test-env.js
├── .babelrc
├── .eslintrc.json
├── index.ejs
└── index.js
├── .gitignore
├── etc
├── ssh
│ └── config
└── lighttpd
│ └── lighttpd.conf
├── server
├── src
│ ├── middleware.php
│ ├── routes.php
│ ├── Service
│ │ ├── ConnectionServiceInterface.php
│ │ └── ConnectionService.php
│ ├── Dao
│ │ └── UserDao.php
│ ├── settings.php
│ ├── AppErrorHandler.php
│ ├── Middleware
│ │ └── ConnectionManagerMiddleware.php
│ ├── Action
│ │ └── InteractionAction.php
│ └── dependencies.php
├── public
│ └── index.php
├── phpcs.xml
├── tests
│ └── Service
│ │ └── ConnectionServiceTest.php
├── phpunit.xml
└── composer.json
├── .editorconfig
├── Dockerfile
├── i18n
├── bs.json
├── mnw.json
├── azb.json
├── ms-arab.json
├── hy.json
├── kn.json
├── skr-arab.json
├── hif-latn.json
├── smn.json
├── sms.json
├── it.json
├── kum.json
├── zgh.json
├── ne.json
├── bg.json
├── my.json
├── bn.json
├── ckb.json
├── kab.json
├── bcl.json
├── zh-hans.json
├── li.json
├── zh-hant.json
├── ko.json
├── cs.json
├── ja.json
├── th.json
├── he.json
├── ar.json
├── en.json
├── ps.json
├── xmf.json
├── lt.json
├── eo.json
├── te.json
├── sl.json
├── ce.json
├── fa.json
├── nb.json
├── ast.json
├── sh.json
├── anp.json
├── diq.json
├── sv.json
├── jv-java.json
├── sr-ec.json
├── da.json
├── fi.json
├── hi.json
├── krc.json
├── be-tarask.json
├── hyw.json
├── tl.json
├── sk.json
├── pl.json
├── mk.json
├── gl.json
├── id.json
├── nl.json
├── sc.json
├── pt-br.json
├── ru.json
├── vi.json
└── pt.json
├── toolinfo.json
├── .env.dist
├── docker-compose.yml
├── bin
└── start
└── .github
└── workflows
└── build.yml
/client/.eslintignore:
--------------------------------------------------------------------------------
1 | /html
2 |
--------------------------------------------------------------------------------
/client/__mocks__/style.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/client/i18n.dir.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | path: '../i18n',
3 | filter: /\.json$/
4 | };
5 |
--------------------------------------------------------------------------------
/client/bin/watch:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | npm install --verbose --unsafe-perm \
3 | && npm run watch
4 |
--------------------------------------------------------------------------------
/client/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wikimedia/InteractionTimeline/HEAD/client/images/logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /client/coverage
2 | /client/node_modules
3 | /server/vendor
4 | /html
5 | .env
6 | /etc/ssh/id_rsa
7 |
--------------------------------------------------------------------------------
/etc/ssh/config:
--------------------------------------------------------------------------------
1 | Host *
2 | UseRoaming no
3 | ServerAliveInterval 30
4 | AddressFamily inet
5 | IdentityFile /root/.ssh/id_rsa
6 |
--------------------------------------------------------------------------------
/server/src/middleware.php:
--------------------------------------------------------------------------------
1 | add( new App\Middleware\ConnectionManagerMiddleware(
4 | $app->getContainer()->get( 'connectionService' )
5 | ) );
6 |
--------------------------------------------------------------------------------
/client/src/components/timeline/revision/edit-summary-parts.js:
--------------------------------------------------------------------------------
1 | const REGEX_EDIT_SUMMARY_PARTS = /(?:\/\*([^*]+)\*\/)?(.+)?/;
2 |
3 | export default REGEX_EDIT_SUMMARY_PARTS;
4 |
--------------------------------------------------------------------------------
/client/test-env.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | // Configure Enzyme
5 | Enzyme.configure( { adapter: new Adapter() } );
6 |
--------------------------------------------------------------------------------
/server/src/routes.php:
--------------------------------------------------------------------------------
1 | group( '/{project}', function (){
4 | $this->get( '/interaction', App\Action\InteractionAction::class )
5 | ->setName( 'interaction' );
6 | } );
7 |
--------------------------------------------------------------------------------
/client/src/entities/wiki-namespace.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 |
3 | export default class WikiNamespace extends Record( {
4 | id: undefined,
5 | name: undefined
6 | }, 'WikiNamespace' ) {}
7 |
--------------------------------------------------------------------------------
/client/src/entities/page.js:
--------------------------------------------------------------------------------
1 | import { Record, Map } from 'immutable';
2 |
3 | export default class Page extends Record( {
4 | id: undefined,
5 | title: undefined,
6 | editors: new Map()
7 | }, 'Page' ) {}
8 |
--------------------------------------------------------------------------------
/client/src/entities/diff-meta.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 |
3 | export default class DiffMeta extends Record( {
4 | show: false,
5 | status: 'ready',
6 | error: undefined
7 | }, 'DiffMeta' ) {}
8 |
--------------------------------------------------------------------------------
/client/src/entities/wiki.js:
--------------------------------------------------------------------------------
1 | import { Record, Map } from 'immutable';
2 |
3 | export default class Wiki extends Record( {
4 | id: undefined,
5 | domain: undefined,
6 | namespaces: new Map()
7 | }, 'Wiki' ) {}
8 |
--------------------------------------------------------------------------------
/client/src/entities/revision-meta.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 |
3 | export default class RevisionMeta extends Record( {
4 | status: 'done',
5 | error: undefined,
6 | interaction: true
7 | }, 'RevisionMeta' ) {}
8 |
--------------------------------------------------------------------------------
/client/src/components/timeline/user-list.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import UserList from './user-list';
3 |
4 | export default connect(
5 | ( state ) => ( {
6 | users: state.query.user
7 | } )
8 | )( UserList );
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.md]
11 | trim_trailing_whitespace = false
12 |
13 | [*.{yaml,yml}]
14 | indent_style = space
15 |
--------------------------------------------------------------------------------
/client/src/reducers/revisions/error.js:
--------------------------------------------------------------------------------
1 | export default ( state = null, action ) => {
2 | switch ( action.type ) {
3 | case 'REVISIONS_ERROR':
4 | return action.error;
5 | case 'REVISIONS_ERROR_CLEAR':
6 | return null;
7 | default:
8 | return state;
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/client/src/selectors/users.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import { OrderedSet } from 'immutable';
3 |
4 | const getUsers = createSelector(
5 | state => state.query.user,
6 | ( user = new OrderedSet() ) => user.toArray()
7 | );
8 |
9 | export default getUsers;
10 |
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "targets": {
5 | "node": "6.11",
6 | "browsers": ["last 2 versions"]
7 | }
8 | }]
9 | ],
10 | "plugins": [
11 | "transform-react-jsx",
12 | "transform-object-rest-spread"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/components/timeline/status.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { getStatus } from 'app/selectors/status';
3 | import Status from './status';
4 |
5 | export default connect(
6 | state => ( {
7 | status: getStatus( state )
8 | } )
9 | )( Status );
10 |
--------------------------------------------------------------------------------
/client/src/components/timeline/timeline.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Timeline from './timeline';
3 |
4 | export default connect(
5 | state => ( {
6 | empty: state.revisions.list.isEmpty(),
7 | status: state.revisions.status
8 | } )
9 | )( Timeline );
10 |
--------------------------------------------------------------------------------
/client/src/reducers/revisions/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import list from './list';
3 | import status from './status';
4 | import cont from './cont';
5 | import error from './error';
6 |
7 | export default combineReducers( {
8 | list,
9 | status,
10 | cont,
11 | error
12 | } );
13 |
--------------------------------------------------------------------------------
/client/src/utils/location-query.js:
--------------------------------------------------------------------------------
1 | import qs from 'querystring';
2 | import Query from 'app/entities/query';
3 |
4 | export default ( location ) => {
5 | let data = {};
6 |
7 | if ( location.search ) {
8 | data = qs.parse( location.search.substring( 1 ) );
9 | }
10 |
11 | return new Query( data );
12 | };
13 |
--------------------------------------------------------------------------------
/client/src/components/timeline/date-list.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { getTimelineRevisions } from 'app/selectors/revisions';
3 | import DateList from './date-list';
4 |
5 | export default connect(
6 | state => ( {
7 | revisions: getTimelineRevisions( state )
8 | } )
9 | )( DateList );
10 |
--------------------------------------------------------------------------------
/client/src/reducers/wikis.js:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 |
3 | export default ( state = new Map(), action ) => {
4 | switch ( action.type ) {
5 | case 'WIKIS_SET':
6 | return action.wikis;
7 | case 'WIKIS_SET_NAMESPACES':
8 | return state.setIn( [ action.id, 'namespaces' ], action.namespaces );
9 | default:
10 | return state;
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/client/src/components/timeline/diff/diff.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { setDiffShow } from 'app/actions/diff';
3 | import Diff from './diff';
4 |
5 | export default connect(
6 | undefined,
7 | ( dispatch, props ) => ( {
8 | closeDiff: () => {
9 | return dispatch( setDiffShow( props.diff, false ) );
10 | }
11 | } )
12 | )( Diff );
13 |
--------------------------------------------------------------------------------
/client/src/reducers/revisions/cont.js:
--------------------------------------------------------------------------------
1 | export default ( state = '', action ) => {
2 | switch ( action.type ) {
3 | case 'REVISIONS_ADD':
4 | return action.cont;
5 | case 'QUERY_USER_CHANGE':
6 | case 'QUERY_WIKI_CHANGE':
7 | case 'QUERY_START_DATE_CHANGE':
8 | case 'QUERY_END_DATE_CHANGE':
9 | return '';
10 | default:
11 | return state;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/server/public/index.php:
--------------------------------------------------------------------------------
1 | run();
16 |
--------------------------------------------------------------------------------
/client/src/actions/wiki.js:
--------------------------------------------------------------------------------
1 | export function setWikis( wikis ) {
2 | return {
3 | type: 'WIKIS_SET',
4 | wikis
5 | };
6 | }
7 |
8 | export function setWikiNamespaces( id, namespaces ) {
9 | return {
10 | type: 'WIKIS_SET_NAMESPACES',
11 | id,
12 | namespaces
13 | };
14 | }
15 |
16 | export function fetchWikiList() {
17 | return {
18 | type: 'WIKI_LIST_FETCH'
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM docker-registry.tools.wmflabs.org/toolforge-php72-sssd-web
2 |
3 | # Set the host domain on Linux.
4 | # see https://github.com/docker/for-linux/issues/264
5 | RUN apt-get update && apt-get install -y \
6 | iputils-ping \
7 | iproute2 \
8 | --no-install-recommends && rm -r /var/lib/apt/lists/*
9 |
10 | ENV COMPOSER_ALLOW_SUPERUSER 1
11 |
12 | CMD ["./var/www/bin/start"]
13 |
--------------------------------------------------------------------------------
/client/src/components/link.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const Link = ( props ) => {
5 | const Tag = props.href ? 'a' : 'span';
6 |
7 | return (
8 |
9 | );
10 | };
11 |
12 | Link.propTypes = {
13 | href: PropTypes.string
14 | };
15 |
16 | Link.defaultProps = {
17 | href: undefined
18 | };
19 |
20 | export default Link;
21 |
--------------------------------------------------------------------------------
/client/src/components/timeline/error-message.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { clearError } from 'app/actions/revisions';
3 | import ErrorMessage from './error-message';
4 |
5 | export default connect(
6 | state => ( {
7 | error: state.revisions.error
8 | } ),
9 | dispatch => ( {
10 | clearError: () => dispatch( clearError() )
11 | } )
12 | )( ErrorMessage );
13 |
--------------------------------------------------------------------------------
/client/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routerReducer } from 'react-router-redux';
3 | import query from './query';
4 | import wikis from './wikis';
5 | import diffs from './diffs';
6 | import revisions from './revisions';
7 |
8 | export default combineReducers( {
9 | router: routerReducer,
10 | query,
11 | wikis,
12 | revisions,
13 | diffs
14 | } );
15 |
--------------------------------------------------------------------------------
/server/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | .
7 |
8 |
9 | vendor/
10 |
11 |
--------------------------------------------------------------------------------
/client/src/components/timeline/user.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { getWiki } from 'app/selectors/wiki';
3 | import makeGetSide from 'app/selectors/side';
4 | import User from './user';
5 |
6 | const getSide = makeGetSide();
7 |
8 | export default connect(
9 | ( state, props ) => ( {
10 | wiki: getWiki( state ),
11 | side: getSide( state, props )
12 | } ),
13 | )( User );
14 |
--------------------------------------------------------------------------------
/client/src/selectors/date.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import moment from 'moment';
3 |
4 | export const getStartDate = createSelector(
5 | state => state.query.startDate,
6 | ( startDate ) => startDate ? moment.unix( startDate ).utc() : null
7 | );
8 |
9 | export const getEndDate = createSelector(
10 | state => state.query.endDate,
11 | ( endDate ) => endDate ? moment.unix( endDate ).utc() : null
12 | );
13 |
--------------------------------------------------------------------------------
/client/src/components/timeline/date-revisions.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { fetchRevisions } from 'app/actions/revisions';
3 | import DateRevisions from './date-revisions';
4 |
5 | export default connect(
6 | state => ( {
7 | empty: state.revisions.list.isEmpty(),
8 | status: state.revisions.status
9 | } ),
10 | dispatch => ( {
11 | fetchList: () => dispatch( fetchRevisions() )
12 | } ),
13 | )( DateRevisions );
14 |
--------------------------------------------------------------------------------
/client/src/components/timeline/user-list.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Set } from 'immutable';
4 | import UserContainer from './user.container';
5 |
6 | const UserList = ( { users } ) => (
7 | users.map( ( user ) => (
8 |
9 | ) ).toArray()
10 | );
11 |
12 | UserList.propTypes = {
13 | users: PropTypes.instanceOf( Set ).isRequired
14 | };
15 |
16 | export default UserList;
17 |
--------------------------------------------------------------------------------
/server/tests/Service/ConnectionServiceTest.php:
--------------------------------------------------------------------------------
1 | createMock( \Doctrine\DBAL\Configuration::class );
10 | $service = new ConnectionService( [], $config );
11 |
12 | $this->expectException( \Exception::class );
13 | $service->getConnection();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/components/timeline/spinner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import 'spinkit/scss/spinners/7-three-bounce.scss';
3 |
4 | const Spinner = () => (
5 |
12 | );
13 |
14 | export default Spinner;
15 |
--------------------------------------------------------------------------------
/client/src/entities/diff.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 | import DiffMeta from './diff-meta';
3 |
4 | export default class Diff extends Record( {
5 | body: undefined,
6 | fromuser: undefined,
7 | fromrevid: undefined,
8 | touser: undefined,
9 | torevid: undefined,
10 | meta: new DiffMeta()
11 | }, 'Diff' ) {
12 | constructor( data = {} ) {
13 | data = {
14 | ...data,
15 | meta: new DiffMeta( data.meta )
16 | };
17 |
18 | super( data );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/components/timeline/diff/header.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { makeGetFromRevision, makeGetToRevision } from 'app/selectors/diff';
3 | import Header from './header';
4 |
5 | const getFromRevision = makeGetFromRevision();
6 | const getToRevision = makeGetToRevision();
7 |
8 | export default connect(
9 | ( state, props ) => ( {
10 | from: getFromRevision( state, props ),
11 | to: getToRevision( state, props )
12 | } )
13 | )( Header );
14 |
--------------------------------------------------------------------------------
/client/src/components/fields/select-users.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { OrderedSet } from 'immutable';
3 | import { userChange } from 'app/actions/query';
4 | import getUsers from 'app/selectors/users';
5 | import SelectUsers from './select-users';
6 |
7 | export default connect(
8 | state => ( {
9 | value: getUsers( state )
10 | } ),
11 | dispatch => ( {
12 | onChange: value => dispatch( userChange( new OrderedSet( value ) ) )
13 | } ),
14 | )( SelectUsers );
15 |
--------------------------------------------------------------------------------
/i18n/bs.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Srdjan m",
5 | "Srđan"
6 | ]
7 | },
8 | "app-title": "Hronologija interakcije",
9 | "field-label-end-date": "Završni datum",
10 | "field-label-start-date": "Početni datum",
11 | "field-label-users": "Korisnici",
12 | "field-label-wiki": "Wiki",
13 | "field-select-placeholder": "Izaberite...",
14 | "field-select-no-results": "Nema rezultata",
15 | "report-bug": "Prijavi grešku",
16 | "view-source-code-on": "Vidi na $1"
17 | }
18 |
--------------------------------------------------------------------------------
/toolinfo.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "interaction-timeline",
3 | "title" : "Interaction Timeline",
4 | "description" : "Shows a chronological history for two users on pages where they have both made edits.",
5 | "url" : "https://meta.wikimedia.org/wiki/Community_health_initiative/Interaction_Timeline",
6 | "keywords" : "anti-harassment, javascript, php, wikipedia",
7 | "author" : "David Barratt, Dayllan Maza",
8 | "repository" : "https://github.com/wikimedia/InteractionTimeline"
9 | }
10 |
--------------------------------------------------------------------------------
/i18n/mnw.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Aue Nai",
5 | "咽頭べさ"
6 | ]
7 | },
8 | "app-feedback-link": "ရီု",
9 | "back-top": "ကလေင် တိုန် နူစ",
10 | "error": "ဗၠေတ်",
11 | "field-label-end-date": "တ္ၚဲတုဲဒှ်",
12 | "field-label-start-date": "တ္ၚဲစပ္တမ်",
13 | "field-label-users": "ညးလွပ်",
14 | "field-label-wiki": "ဝဳကဳ",
15 | "field-select-placeholder": "ရုဲကေတ်",
16 | "field-select-no-results": "အရာမဂၠာဲဂှ် မုဟွံဂွံ ဆဵု",
17 | "discuss-on-wiki-copy": "စၠောအ်ကပ်ပဳ"
18 | }
19 |
--------------------------------------------------------------------------------
/i18n/azb.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Alp Er Tunqa"
5 | ]
6 | },
7 | "back-top": "اوسته قاییت",
8 | "error": "خطا",
9 | "field-label-end-date": "قورتارماق تاریخی",
10 | "field-label-start-date": "باشلانماق تاریخی",
11 | "field-label-users": "ایشلدنلر",
12 | "field-label-wiki": "ویکی",
13 | "field-select-placeholder": "سئچین...",
14 | "field-select-no-results": "هئچ بیر نتیجه تاپیلمادی",
15 | "privacy-policy": "گیزلیلیک سیاستی",
16 | "warning-no-results": "نتیجه یوخدور"
17 | }
18 |
--------------------------------------------------------------------------------
/i18n/ms-arab.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Tofeiku"
5 | ]
6 | },
7 | "app-feedback-link": "بنتوان",
8 | "back-top": "کمبالي کأتس",
9 | "error": "رالت",
10 | "field-label-end-date": "تاريخ تمت",
11 | "field-label-start-date": "تاريخ مولا",
12 | "field-label-users": "ڤڠݢونا",
13 | "field-label-wiki": "ويکي",
14 | "field-select-placeholder": "ڤيليه...",
15 | "privacy-policy": "داسر ڤريۏاسي",
16 | "discuss-on-wiki-copy": "سالين",
17 | "discuss-on-wiki-copied": "دسالين!"
18 | }
19 |
--------------------------------------------------------------------------------
/.env.dist:
--------------------------------------------------------------------------------
1 | DEBUG=true
2 |
3 | # Use DB_HOST when connecting to the replicas through an SSH tunnel.
4 | # Leave as host.docker.internal if the tunnel is running on the host machine.
5 | DB_HOST=host.docker.internal
6 |
7 | # Use DB_CLUSTER when connecting directly to the replicas. The wiki id will be
8 | # prepended to the DB_CLUSTER.
9 | # DB_CLUSTER=web.db.svc.eqiad.wmflabs
10 |
11 | DB_PORT=3306
12 | DB_USER=
13 | DB_PASS=
14 |
15 | REDIS_HOST=redis
16 | REDIS_PORT=6379
17 | REDIS_KEY_PREFIX=interaction-timeline
18 |
--------------------------------------------------------------------------------
/client/src/selectors/wiki.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import { Map } from 'immutable';
3 |
4 | export const getWikiOptions = createSelector(
5 | state => state.wikis,
6 | ( wikis = new Map() ) => {
7 | return wikis.map( ( wiki ) => ( {
8 | value: wiki.id,
9 | label: `${wiki.domain}`
10 | } ) ).toArray();
11 | }
12 | );
13 |
14 | export const getWiki = createSelector(
15 | state => state.wikis,
16 | state => state.query.wiki,
17 | ( wikis = new Map(), id = '' ) => wikis.get( id )
18 | );
19 |
--------------------------------------------------------------------------------
/server/src/Service/ConnectionServiceInterface.php:
--------------------------------------------------------------------------------
1 | {
5 | const subject = new Subject();
6 |
7 | const callback = ( entries ) => {
8 | entries.forEach( entry => {
9 | subject.next( entry );
10 | } );
11 | };
12 |
13 | const observer = new IntersectionObserver( callback, options );
14 |
15 | observer.observe( element );
16 |
17 | return subject;
18 | };
19 |
20 | export default createIntersectionObservable;
21 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | web:
4 | build: ./
5 | env_file:
6 | - .env
7 | ports:
8 | - 8888:80
9 | volumes:
10 | - ./etc/lighttpd/lighttpd.conf:/etc/lighttpd/lighttpd.conf:cached
11 | - ./:/var/www:cached
12 | links:
13 | - redis
14 | watch:
15 | image: docker-registry.tools.wmflabs.org/toolforge-node10-sssd-web
16 | working_dir: /code/client
17 | command:
18 | - ./bin/watch
19 | volumes:
20 | - ./:/code:cached
21 | redis:
22 | image: redis
23 |
--------------------------------------------------------------------------------
/i18n/hy.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Kareyac"
5 | ]
6 | },
7 | "app-feedback-link": "Օգնություն",
8 | "error": "Սխալ",
9 | "error-help-refresh": "թարմացնել էջը",
10 | "field-label-users": "Մասնակիցներ",
11 | "field-label-wiki": "Վիքի",
12 | "field-select-placeholder": "Ընտրեք...",
13 | "field-select-no-results": "Ոչինչ չի գտնվել",
14 | "privacy-policy": "Գաղտնիության քաղաքականություն",
15 | "warning-no-results": "Արդյունք չկա",
16 | "discuss-on-wiki": "Կիսվել այս արդյունքներով",
17 | "discuss-on-wiki-copy": "Պատճենել"
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/utils/location-query.test.js:
--------------------------------------------------------------------------------
1 | import Query from 'app/entities/query';
2 | import locationQuery from './location-query';
3 |
4 | test( 'will return a Query object', () => {
5 | const wiki = 'testwiki';
6 | const location = {
7 | search: `?wiki=${wiki}`
8 | };
9 |
10 | let query;
11 |
12 | query = locationQuery( location );
13 | expect( query ).toBeInstanceOf( Query );
14 | expect( query.wiki ).toEqual( wiki );
15 |
16 | query = locationQuery( {} );
17 | expect( query ).toBeInstanceOf( Query );
18 | expect( query.wiki ).toEqual( undefined );
19 | } );
20 |
--------------------------------------------------------------------------------
/i18n/kn.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"
5 | ]
6 | },
7 | "app-feedback-link": "ಸಹಾಯ",
8 | "error": "ದೋಷ",
9 | "error-help-report": "ದೋಷವನ್ನು ವರದಿ ಮಾಡಿ",
10 | "field-label-end-date": "ಮುಕ್ತಾಯದ ದಿನಾಂಕ",
11 | "field-label-start-date": "ಆರಂಭದ ದಿನಾಂಕ",
12 | "field-label-users": "ಬಳಕೆದಾರರು",
13 | "field-label-wiki": "ವಿಕಿ",
14 | "field-select-no-results": "ಯಾವುದೇ ಫಲಿತಾಂಶಗಳಿಲ್ಲ",
15 | "privacy-policy": "ಗೌಪ್ಯತಾ ನೀತಿ",
16 | "revision-edit-summary-removed": "ಸಂಪಾದನೆಯ ಸಾರಾಂಶ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
17 | "view-source-code-on": "$1 ನಲ್ಲಿ ನೋಡಿ"
18 | }
19 |
--------------------------------------------------------------------------------
/server/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ./tests/
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/server/src/Dao/UserDao.php:
--------------------------------------------------------------------------------
1 | conn->createQueryBuilder();
14 | $query->select( 'user_id' )
15 | ->from( 'user' )
16 | ->where( 'user_name = :user' )
17 | ->setParameter( ':user', $username, \PDO::PARAM_STR );
18 |
19 | $stmt = $query->execute();
20 | $userId = $stmt->fetchColumn();
21 |
22 | return $userId;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/components/timeline/revision/timelapse.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Message } from '@wikimedia/react.i18n';
4 |
5 | const TimelineTimelapse = ( { date, samePage } ) => (
6 |
7 |
8 |
9 | );
10 |
11 | TimelineTimelapse.propTypes = {
12 | date: PropTypes.string.isRequired,
13 | samePage: PropTypes.bool.isRequired
14 | };
15 |
16 | export default TimelineTimelapse;
17 |
--------------------------------------------------------------------------------
/bin/start:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Set the host domain on Linux.
4 | # see https://github.com/docker/for-linux/issues/264
5 | HOST_DOMAIN="host.docker.internal";
6 | ping -q -c1 $HOST_DOMAIN > /dev/null 2>&1;
7 | if [ $? -ne 0 ]; then
8 | HOST_IP=$(ip route | awk 'NR==1 {print $3}');
9 | echo "$HOST_IP\t$HOST_DOMAIN" >> /etc/hosts;
10 | fi
11 |
12 | # Install Dependencies and start Lighttpd
13 | composer install -d /var/www/server || exit 1;
14 | mkdir -p /var/www/html/api || exit 1;
15 | ln -sf ../../server/public/index.php /var/www/html/api/index.php || exit 1;
16 | lighttpd -D -f /etc/lighttpd/lighttpd.conf || exit 1;
17 |
--------------------------------------------------------------------------------
/client/src/components/timeline/date.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import 'material-design-icons/iconfont/material-icons.css';
4 |
5 | const TimelineDate = ( { date } ) => (
6 |
13 | );
14 |
15 | TimelineDate.propTypes = {
16 | date: PropTypes.string.isRequired
17 | };
18 |
19 | export default TimelineDate;
20 |
--------------------------------------------------------------------------------
/client/src/selectors/side.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const makeGetSide = () => (
4 | createSelector(
5 | state => state.query.user,
6 | ( _, props ) => {
7 | if ( props.user ) {
8 | return props.user;
9 | }
10 |
11 | if ( props.revision ) {
12 | return props.revision.user;
13 | }
14 | },
15 | ( users, user ) => {
16 | let side;
17 |
18 | if ( user === users.first() ) {
19 | side = 'left';
20 | } else if ( user === users.last() ) {
21 | side = 'right';
22 | }
23 |
24 | return side;
25 | }
26 | )
27 | );
28 |
29 | export default makeGetSide;
30 |
--------------------------------------------------------------------------------
/i18n/skr-arab.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Saraiki"
5 | ]
6 | },
7 | "app-feedback-link": "مدد",
8 | "error": "خرابی",
9 | "error-help": "$1، تے ول $2۔",
10 | "field-label-end-date": "ختم کرݨ دی تریخ",
11 | "field-label-start-date": "شروع کرݨ دی تریخ",
12 | "field-label-users": "ورتݨ آلے",
13 | "field-label-wiki": "وکی",
14 | "field-select-placeholder": "چُݨو۔۔۔",
15 | "field-select-search-prompt": "ڳولݨ کیتے لکھو",
16 | "view-source-code-on": "$1 تے ݙیکھو",
17 | "warning-no-results": "کوئی نتیجہ کائنی۔",
18 | "discuss-on-wiki-copy": "نقل کرو",
19 | "discuss-on-wiki-copied": "نقل تھی ڳئے!"
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/components/timeline/revision/title.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import REGEX_EDIT_SUMMARY_PARTS from './edit-summary-parts';
3 |
4 | const Title = ( { title, comment } ) => {
5 | if ( !comment ) {
6 | return title;
7 | }
8 |
9 | const matches = comment.match( REGEX_EDIT_SUMMARY_PARTS );
10 |
11 | if ( matches[ 1 ] ) {
12 | return title + ' § ' + matches[ 1 ].trim();
13 | }
14 |
15 | return title;
16 | };
17 |
18 | Title.propTypes = {
19 | title: PropTypes.string.isRequired,
20 | comment: PropTypes.string
21 | };
22 |
23 | Title.defaultProps = {
24 | comment: undefined
25 | };
26 |
27 | export default Title;
28 |
--------------------------------------------------------------------------------
/client/src/components/fields/select-wiki.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { fetchWikiList } from 'app/actions/wiki';
3 | import { getWikiOptions } from 'app/selectors/wiki';
4 | import { wikiChange } from 'app/actions/query';
5 | import SelectWiki from './select-wiki';
6 |
7 | export default connect(
8 | state => ( {
9 | isLoading: !state.wikis.size,
10 | value: state.query.wiki,
11 | options: getWikiOptions( state )
12 | } ),
13 | dispatch => ( {
14 | onChange: value => dispatch( wikiChange( value ? value.value : undefined ) ),
15 | fetchOptions: () => dispatch( fetchWikiList() )
16 | } ),
17 | )( SelectWiki );
18 |
--------------------------------------------------------------------------------
/client/src/entities/revision.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 | import moment from 'moment';
3 | import RevisionMeta from './revision-meta';
4 |
5 | export default class Revision extends Record( {
6 | id: undefined,
7 | pageId: undefined,
8 | pageNamespace: undefined,
9 | title: undefined,
10 | user: undefined,
11 | timestamp: moment(),
12 | minor: false,
13 | sizeDiff: 0,
14 | comment: undefined,
15 | commentHidden: false,
16 | suppressed: false,
17 | meta: new RevisionMeta()
18 | }, 'Revision' ) {
19 | constructor( data = {} ) {
20 | data = {
21 | ...data,
22 | meta: new RevisionMeta( data.meta )
23 | };
24 |
25 | super( data );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/components/timeline/diff/header-revision.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { fetchRevision } from 'app/actions/revisions';
3 | import { makeGetRevisionUrl } from 'app/selectors/revisions';
4 | import makeGetSide from 'app/selectors/side';
5 | import HeaderRevision from './header-revision';
6 |
7 | const getSide = makeGetSide();
8 | const getRevisionUrl = makeGetRevisionUrl();
9 |
10 | export default connect(
11 | ( state, props ) => ( {
12 | side: getSide( state, props ),
13 | url: getRevisionUrl( state, props )
14 | } ),
15 | dispatch => ( {
16 | fetchRevision: id => dispatch( fetchRevision( id ) )
17 | } ),
18 | )( HeaderRevision );
19 |
--------------------------------------------------------------------------------
/client/src/epics/index.js:
--------------------------------------------------------------------------------
1 | import { combineEpics } from 'redux-observable';
2 | import { pushQueryToLocation, pushLocationToQuery, setDefaultQueryOnLoad } from './query';
3 | import { fetchAllWikis, fetchWikiNamespaces } from './wiki';
4 | import {
5 | shouldFetchRevisions,
6 | revisionsReady,
7 | fetchRevision,
8 | doFetchRevisions
9 | } from './revisions';
10 | import { fetchDiff } from './diff';
11 |
12 | export default combineEpics(
13 | pushQueryToLocation,
14 | pushLocationToQuery,
15 | fetchAllWikis,
16 | fetchWikiNamespaces,
17 | shouldFetchRevisions,
18 | revisionsReady,
19 | fetchRevision,
20 | doFetchRevisions,
21 | fetchDiff,
22 | setDefaultQueryOnLoad
23 | );
24 |
--------------------------------------------------------------------------------
/client/src/selectors/status.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const getStatus = createSelector(
4 | state => state.revisions.status,
5 | state => state.query.user,
6 | state => state.query.wiki,
7 | state => state.revisions.list,
8 | ( status, users, wiki, revisions ) => {
9 | if ( status === 'done' && revisions.isEmpty() ) {
10 | return 'noresults';
11 | } else if ( status === 'notready' ) {
12 | if ( wiki && users.count() < 2 ) {
13 | return 'nousers';
14 | }
15 |
16 | if ( !wiki && users.count() >= 2 ) {
17 | return 'nowiki';
18 | }
19 | }
20 |
21 | return status;
22 | }
23 | );
24 |
25 | export default getStatus;
26 |
--------------------------------------------------------------------------------
/client/src/components/timeline/revision/byte-change.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const ByteChange = ( { sizeDiff, minor } ) => {
5 | let size = '(' + ( sizeDiff > 0 ? '+' : '' ) + sizeDiff + ')';
6 |
7 | if ( sizeDiff > 500 || sizeDiff < -500 ) {
8 | size = {size} ;
9 | }
10 |
11 | let displayMinor;
12 |
13 | if ( minor ) {
14 | displayMinor = m ;
15 | }
16 |
17 | return (
18 | {displayMinor} {size}
19 | );
20 | };
21 |
22 | ByteChange.propTypes = {
23 | sizeDiff: PropTypes.number.isRequired,
24 | minor: PropTypes.bool.isRequired
25 | };
26 |
27 | export default ByteChange;
28 |
--------------------------------------------------------------------------------
/client/src/components/fields/date-range.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { startDateChange, endDateChange } from 'app/actions/query';
3 | import { getStartDate, getEndDate } from 'app/selectors/date';
4 | import DateRange from './date-range';
5 |
6 | export default connect(
7 | state => ( {
8 | startDate: getStartDate( state ),
9 | endDate: getEndDate( state )
10 | } ),
11 | dispatch => ( {
12 | onStartDateChange: value => dispatch( startDateChange( value ? value.utc().unix().toString() : undefined ) ),
13 | onEndDateChange: value => dispatch( endDateChange( value ? value.utc().endOf( 'day' ).unix().toString() : undefined ) )
14 | } ),
15 | )( DateRange );
16 |
--------------------------------------------------------------------------------
/client/src/components/timeline/diff/header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import RevisionEntity from 'app/entities/revision';
4 | import RevisionHeaderContainer from './header-revision.container';
5 |
6 | const Header = ( { from, to } ) => {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | Header.propTypes = {
16 | from: PropTypes.instanceOf( RevisionEntity ),
17 | to: PropTypes.instanceOf( RevisionEntity )
18 | };
19 |
20 | Header.defaultProps = {
21 | from: undefined,
22 | to: undefined
23 | };
24 |
25 | export default Header;
26 |
--------------------------------------------------------------------------------
/client/src/components/share/share.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { getStartDate, getEndDate } from 'app/selectors/date.js';
3 | import getUsers from 'app/selectors/users.js';
4 | import getEditorInteractUrl from 'app/selectors/editorinteract';
5 | import { getWiki } from 'app/selectors/wiki.js';
6 | import Share from './share';
7 |
8 | export default connect(
9 | state => ( {
10 | empty: state.revisions.list.isEmpty(),
11 | queryString: state.router.location.search,
12 | startDate: getStartDate( state ),
13 | endDate: getEndDate( state ),
14 | users: getUsers( state ),
15 | wiki: getWiki( state ),
16 | editorInteractUrl: getEditorInteractUrl( state )
17 | } )
18 | )( Share );
19 |
--------------------------------------------------------------------------------
/client/src/actions/diff.js:
--------------------------------------------------------------------------------
1 | export function throwDiffError( diff, error ) {
2 | return {
3 | type: 'DIFFS_THROW_ERROR',
4 | diff,
5 | error
6 | };
7 | }
8 |
9 | export function setDiff( diff ) {
10 | return {
11 | type: 'DIFFS_SET',
12 | diff
13 | };
14 | }
15 |
16 | export function setDiffShow( diff, show, suppressed = false ) {
17 | return {
18 | type: 'DIFFS_SHOW_SET',
19 | diff,
20 | show,
21 | suppressed
22 | };
23 | }
24 |
25 | export function toggleDiff( diff, suppressed = false ) {
26 | return setDiffShow( diff, !diff.meta.show, suppressed );
27 | }
28 |
29 | export function setDiffStatus( diff, status ) {
30 | return {
31 | type: 'DIFFS_STATUS_SET',
32 | diff,
33 | status
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/i18n/hif-latn.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Thakurji"
5 | ]
6 | },
7 | "app-feedback-link": "Madat",
8 | "error": "Galti",
9 | "error-help": "$1, aur fir $2.",
10 | "error-help-report": "Error ke report karo",
11 | "error-help-refresh": "panna ke refresh karo",
12 | "error-message-request-url": "URL ke request karo",
13 | "field-label-end-date": "Khalaas waala taarik",
14 | "field-label-start-date": "Suruu waala taarik",
15 | "field-label-users": "Sadasya",
16 | "field-label-wiki": "Wiki",
17 | "field-select-placeholder": "Select karo...",
18 | "field-select-no-results": "Koi result nai milaa",
19 | "discuss-on-wiki-copy": "Copy karo",
20 | "discuss-on-wiki-copied": "Copy kar lewa gais hai!"
21 | }
22 |
--------------------------------------------------------------------------------
/i18n/smn.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Muotâ",
5 | "Seipinne",
6 | "Yupik"
7 | ]
8 | },
9 | "app-feedback-link": "Iše",
10 | "back-top": "Maassâd pajas",
11 | "error": "Feilâ",
12 | "error-help-report": "Almoot feeilâ",
13 | "field-label-end-date": "Nuuhâmpeivi",
14 | "field-label-start-date": "Älgimpeivi",
15 | "field-label-users": "Kevtteeh",
16 | "field-label-wiki": "Wiki",
17 | "field-select-placeholder": "Valjii...",
18 | "field-select-no-results": "Iä uuccâmpuátuseh",
19 | "powered-by": "Falâlduv taha máhđulâžžân Wikimedia Toolforge",
20 | "view-source-code-on": "Čääiti $1ist",
21 | "warning-no-results": "Iä puátuseh",
22 | "discuss-on-wiki-copy": "Kopijist",
23 | "discuss-on-wiki-copied": "Kopijistum!"
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/selectors/editorinteract.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import qs from 'querystring';
3 | import getUsers from './users';
4 | import { getStartDate, getEndDate } from './date';
5 |
6 | const getEditorInteractUrl = createSelector(
7 | ( state ) => state.query.wiki,
8 | getUsers,
9 | getStartDate,
10 | getEndDate,
11 | ( server, users, start, end ) => {
12 | const data = {
13 | server,
14 | users
15 | };
16 |
17 | if ( start ) {
18 | data.startDate = start.format( 'YYYYMMDD' );
19 | }
20 |
21 | if ( end ) {
22 | data.endDate = end.format( 'YYYYMMDD' );
23 | }
24 |
25 | return 'https://tools.wmflabs.org/sigma/editorinteract.py?' + qs.stringify( data );
26 | }
27 | );
28 |
29 | export default getEditorInteractUrl;
30 |
--------------------------------------------------------------------------------
/client/src/reducers/query.js:
--------------------------------------------------------------------------------
1 | import Query from 'app/entities/query';
2 |
3 | export default ( state = new Query(), action ) => {
4 | switch ( action.type ) {
5 | case 'QUERY_SET_DEFAULT':
6 | case 'QUERY_UPDATE':
7 | let query = action.query;
8 |
9 | if ( query.user ) {
10 | query = query.set( 'user', query.user.slice( 0, 2 ) );
11 | }
12 |
13 | return query;
14 | case 'QUERY_USER_CHANGE':
15 | return state.set( 'user', action.users );
16 | case 'QUERY_WIKI_CHANGE':
17 | return state.set( 'wiki', action.wiki );
18 | case 'QUERY_START_DATE_CHANGE':
19 | return state.set( 'startDate', action.startDate );
20 | case 'QUERY_END_DATE_CHANGE':
21 | return state.set( 'endDate', action.endDate );
22 | default:
23 | return state;
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/client/src/reducers/revisions/status.js:
--------------------------------------------------------------------------------
1 | export default ( state = 'notready', action ) => {
2 | switch ( action.type ) {
3 | case 'REVISIONS_FETCHING':
4 | return 'fetching';
5 | case 'REVISIONS_READY':
6 | return 'ready';
7 | case 'QUERY_WIKI_CHANGE':
8 | if ( !action.wiki ) {
9 | return 'notready';
10 | }
11 |
12 | return state;
13 | case 'QUERY_USER_CHANGE':
14 | if ( action.users.count() < 2 ) {
15 | return 'notready';
16 | }
17 |
18 | return state;
19 | case 'REVISIONS_NOT_READY':
20 | return 'notready';
21 | case 'REVISIONS_DONE':
22 | return 'done';
23 | case 'REVISIONS_ERROR':
24 | return 'error';
25 | case 'REVISIONS_ADD':
26 | return action.cont === false ? 'done' : 'ready';
27 | default:
28 | return state;
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/i18n/sms.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Yupik"
5 | ]
6 | },
7 | "back-top": "Mååust seeid aʹlǧǧe",
8 | "error": "Vââʹǩǩ",
9 | "error-help": "$1, teä $2.",
10 | "error-help-report": "Iʹlmmet čuõlmâst",
11 | "error-help-refresh": "peiʹvved seeid",
12 | "licensed-under": "Lisenssiõsttum liseeʹnsin $1",
13 | "field-label-start-date": "Äʹlǧǧempeiʹvv",
14 | "field-label-users": "Õõʹnni",
15 | "field-select-placeholder": "Vaʹlljed...",
16 | "field-select-no-results": "Ij käunnʼjam ni mii",
17 | "privacy-policy": "Teâttsuejjčiõʹlǧǧõs",
18 | "powered-by": "Kääzzkõõzz vueiʹtlvâstt Wikimedia Toolforge",
19 | "report-bug": "Iʹlmmet čuõlmâst",
20 | "revision-edit-summary-removed": "õʹhtteǩeässmõš lij jaukkuum",
21 | "warning-no-results": "Ij käunnʼjam ni mii",
22 | "discuss-on-wiki-copy": "Kopiââʹst"
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/selectors/editorinteract.test.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import moment from 'moment';
3 | import './editorinteract';
4 |
5 | jest.mock( 'reselect' );
6 | jest.mock( './users' );
7 | jest.mock( './date' );
8 |
9 | // The last argument of the last call is the function to test.
10 | const call = createSelector.mock.calls[ createSelector.mock.calls.length - 1 ];
11 | const getEditorInteractUrl = call[ call.length - 1 ];
12 |
13 | test( 'returns an editorinteract url', () => {
14 | const url = getEditorInteractUrl( 'testwiki', [ 'Derby pie', 'Sweets lover' ], moment.utc( '1970-01-01T00:00' ), moment.utc( '1999-12-31T23:59' ) );
15 | expect( url ).toEqual( 'https://tools.wmflabs.org/sigma/editorinteract.py?server=testwiki&users=Derby%20pie&users=Sweets%20lover&startDate=19700101&endDate=19991231' );
16 | } );
17 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: push
4 |
5 | jobs:
6 | server:
7 | runs-on: ubuntu-latest
8 | container:
9 | image: docker-registry.tools.wmflabs.org/toolforge-php72-sssd-web
10 | env:
11 | COMPOSER_ALLOW_SUPERUSER: 1
12 | steps:
13 | - uses: actions/checkout@v1
14 | - run: 'composer install'
15 | working-directory: server
16 | - run: 'composer test'
17 | working-directory: server
18 | client:
19 | runs-on: ubuntu-latest
20 | container:
21 | image: docker-registry.tools.wmflabs.org/toolforge-node10-sssd-web
22 | defaults:
23 | run:
24 | working-directory: client
25 | steps:
26 | - uses: actions/checkout@v1
27 | - run: npm ci --verbose --unsafe-perm
28 | - run: npm run build --if-present
29 | - run: npm test
30 |
--------------------------------------------------------------------------------
/server/src/settings.php:
--------------------------------------------------------------------------------
1 | load();
5 |
6 | return [
7 | 'settings' => [
8 | // slim
9 | 'displayErrorDetails' => getenv( 'DEBUG' ),
10 | 'determineRouteBeforeAppMiddleware' => true,
11 | 'db' => [
12 | 'host' => getenv( 'DB_HOST' ),
13 | 'cluster' => getenv( 'DB_CLUSTER' ),
14 | 'user' => getenv( 'DB_USER' ),
15 | 'pass' => getenv( 'DB_PASS' ),
16 | 'port' => getenv( 'DB_PORT' ),
17 | ],
18 | 'redis' => [
19 | 'host' => getenv( 'REDIS_HOST' ),
20 | 'port' => getenv( 'REDIS_PORT' ),
21 | 'prefix' => getenv( 'REDIS_KEY_PREFIX' ),
22 | ],
23 |
24 | // monolog settings
25 | 'logger' => [
26 | 'name' => 'app',
27 | 'path' => getenv( 'LOGGER_PATH' ) ?: 'php://stderr',
28 | 'level' => Monolog\Logger::DEBUG
29 | ],
30 | ],
31 | ];
32 |
--------------------------------------------------------------------------------
/client/src/components/timeline/alert.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import 'material-design-icons/iconfont/material-icons.css';
4 |
5 | const Alert = ( { type, children } ) => {
6 | let icon = type;
7 | let color = type;
8 |
9 | if ( type === 'error' ) {
10 | color = 'danger';
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 | {icon}
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | Alert.propTypes = {
28 | type: PropTypes.oneOf( [ 'warning', 'error', 'info' ] ).isRequired,
29 | children: PropTypes.node.isRequired
30 | };
31 |
32 | export default Alert;
33 |
--------------------------------------------------------------------------------
/i18n/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Beta16"
5 | ]
6 | },
7 | "app-feedback-link": "Aiuto",
8 | "back-top": "Torna all'inizio",
9 | "between-edits": "$1 tra le modifiche",
10 | "error": "Errore",
11 | "error-help-refresh": "aggiorna la pagina",
12 | "field-label-end-date": "Data fine",
13 | "field-label-start-date": "Data inizio",
14 | "field-label-users": "Utenti",
15 | "field-label-wiki": "Wiki",
16 | "field-select-placeholder": "Seleziona...",
17 | "field-select-no-results": "Nessun risultato trovato",
18 | "privacy-policy": "Informativa sulla privacy",
19 | "report-bug": "Segnala un errore",
20 | "view-source-code-on": "Vedi su $1",
21 | "warning-no-results": "Nessun risultato",
22 | "discuss-on-wiki": "Condividi questi risultati",
23 | "discuss-on-wiki-copy": "Copia",
24 | "discuss-on-wiki-copied": "Copiato!"
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/fields/select-wiki.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Select from 'react-select';
4 | import { Message } from '@wikimedia/react.i18n';
5 | import 'react-select/dist/react-select.css';
6 |
7 | class SelectWiki extends React.Component {
8 |
9 | componentDidMount() {
10 | this.props.fetchOptions();
11 | }
12 |
13 | render() {
14 | return (
15 |
16 | }
19 | noResultsText={ }
20 | searchPromptText={ }
21 | matchPos="start"
22 | matchProp="label"
23 | />
24 | );
25 | }
26 | }
27 |
28 | SelectWiki.propTypes = {
29 | fetchOptions: PropTypes.func.isRequired
30 | };
31 |
32 | export default SelectWiki;
33 |
--------------------------------------------------------------------------------
/i18n/kum.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "ArslanX"
5 | ]
6 | },
7 | "app-feedback-link": "Кёмек",
8 | "back-top": "Оьрге къайтмакъ",
9 | "between-edits": "$1 тюзлевлени арасында",
10 | "error": "Янгылыш",
11 | "error-help": "$1, сонгра $2.",
12 | "error-help-report": "Хатаны гьакъында айтмакъ",
13 | "error-help-refresh": "сагьифаны янгыртмакъ",
14 | "error-message-request-url": "Талапны URL",
15 | "field-label-end-date": "Ахыр тархы",
16 | "field-label-start-date": "Баш тархы",
17 | "field-label-users": "Къоллавчулар",
18 | "field-label-wiki": "Вики",
19 | "field-select-placeholder": "Сайла...",
20 | "field-select-no-results": "Бир зат табылмагъан",
21 | "field-select-search-prompt": "Излемек учун яз",
22 | "privacy-policy": "Энчилилик ёругъу",
23 | "view-source-code-on": "$1 сайфада къарамакъ",
24 | "warning-no-results": "Гьасилсиз"
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/timeline/revision/revision.container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { toggleDiff } from 'app/actions/diff';
3 | import { makeGetDiff, makeGetDiffUrl } from 'app/selectors/diff';
4 | import { makeGetTitle } from 'app/selectors/revisions';
5 | import makeGetSide from 'app/selectors/side';
6 | import Revision from './revision';
7 |
8 | const getSide = makeGetSide();
9 | const getTitle = makeGetTitle();
10 | const getDiffUrl = makeGetDiffUrl();
11 | const getDiff = makeGetDiff();
12 |
13 | export default connect(
14 | ( state, props ) => ( {
15 | side: getSide( state, props ),
16 | url: getDiffUrl( state, props ),
17 | diff: getDiff( state, props ),
18 | title: getTitle( state, props )
19 | } ),
20 | dispatch => ( {
21 | toggleDiff: ( diff, suppressed ) => dispatch( toggleDiff( diff, suppressed ) )
22 | } ),
23 | )( Revision );
24 |
--------------------------------------------------------------------------------
/server/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "slim/slim": "^3.0",
4 | "monolog/monolog": "^1.23",
5 | "doctrine/dbal": "^2.5",
6 | "doctrine/cache": "^1.6",
7 | "vlucas/phpdotenv": "^2.5"
8 | },
9 | "require-dev": {
10 | "phpunit/phpunit": "^5.7",
11 | "jakub-onderka/php-parallel-lint": "^0.9.2",
12 | "mediawiki/mediawiki-codesniffer": "^15.0",
13 | "squizlabs/php_codesniffer": "^3.2"
14 | },
15 | "scripts": {
16 | "fix": "phpcbf",
17 | "lint": "parallel-lint . --exclude vendor",
18 | "phpcs": "phpcs -p -s",
19 | "phpunit": "phpunit",
20 | "test": [
21 | "@lint",
22 | "phpunit",
23 | "phpcs"
24 | ]
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "App\\": "src/"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb",
4 | "wikimedia"
5 | ],
6 | "env": {
7 | "jest": true
8 | },
9 | "rules": {
10 | "import/extensions": ["off"],
11 | "import/no-extraneous-dependencies": ["off"],
12 | "import/no-unresolved": ["off"],
13 | "import/no-namespace": ["error"],
14 | "react/jsx-indent-props": [
15 | "error",
16 | "tab"
17 | ],
18 | "jsx-a11y/label-has-for": ["off"],
19 | "jsx-a11y/anchor-is-valid": ["off"],
20 | "no-case-declarations": ["off"],
21 | "one-var": [
22 | "error",
23 | "never"
24 | ],
25 | "react/jsx-filename-extension": [
26 | "error",
27 | {
28 | "extensions": [
29 | ".js"
30 | ]
31 | }
32 | ],
33 | "react/jsx-indent": [
34 | "error",
35 | "tab"
36 | ],
37 | "no-shadow": ["error"]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/components/timeline/revision/comment.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Message } from '@wikimedia/react.i18n';
4 | import REGEX_EDIT_SUMMARY_PARTS from './edit-summary-parts';
5 |
6 | const Comment = ( { comment, commentHidden } ) => {
7 | if ( commentHidden ) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | const matches = comment.match( REGEX_EDIT_SUMMARY_PARTS );
18 |
19 | // return edit summary without section name or empty
20 | if ( matches[ 2 ] ) {
21 | return (
22 |
23 | {matches[ 2 ].trim()}
24 |
25 | );
26 | }
27 |
28 | return null;
29 | };
30 |
31 | Comment.propTypes = {
32 | comment: PropTypes.string.isRequired,
33 | commentHidden: PropTypes.bool.isRequired
34 | };
35 |
36 | export default Comment;
37 |
--------------------------------------------------------------------------------
/client/src/entities/query.js:
--------------------------------------------------------------------------------
1 | import { OrderedSet, Record } from 'immutable';
2 | import { isIPv6Address } from 'app/utils/ip-validator';
3 |
4 | export default class Query extends Record( {
5 | wiki: undefined,
6 | user: new OrderedSet(),
7 | startDate: undefined,
8 | endDate: undefined
9 | }, 'Query' ) {
10 |
11 | constructor( data = {} ) {
12 | let user = new OrderedSet();
13 |
14 | if ( data.user ) {
15 | if ( Array.isArray( data.user ) ) {
16 | user = new OrderedSet( data.user );
17 | } else {
18 | user = new OrderedSet( [ data.user ] );
19 | }
20 |
21 | // we need to uppercase IPv6 addresses to match
22 | // what's returned by the the usercontribs api endpoint
23 | user = user.map( ( value ) => {
24 | return isIPv6Address( value ) ? value.toUpperCase() : value;
25 | } );
26 | }
27 |
28 | super( {
29 | ...data,
30 | user
31 | } );
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/utils/special-wikis-list.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | id: 'testwiki',
4 | domain: 'test.wikipedia.org',
5 | code: 'test',
6 | family: 'wiki'
7 | },
8 | {
9 | id: 'commonswiki',
10 | domain: 'commons.wikimedia.org',
11 | code: 'commons',
12 | family: 'wiki'
13 | },
14 | {
15 | id: 'metawiki',
16 | domain: 'meta.wikimedia.org',
17 | code: 'meta',
18 | family: 'wiki'
19 | },
20 | {
21 | id: 'wikidatawiki',
22 | domain: 'wikidata.org',
23 | code: 'wikidata',
24 | family: 'wiki'
25 | },
26 | {
27 | id: 'specieswiki',
28 | domain: 'species.wikimedia.org',
29 | code: 'species',
30 | family: 'wiki'
31 | },
32 | {
33 | id: 'sourceswiki',
34 | domain: 'wikisource.org',
35 | code: 'sources',
36 | family: 'wiki'
37 | },
38 | {
39 | id: 'mediawikiwiki',
40 | domain: 'www.mediawiki.org',
41 | code: 'mediawiki',
42 | family: 'wiki'
43 | }
44 | ];
45 |
--------------------------------------------------------------------------------
/client/src/components/timeline/user.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Wiki from 'app/entities/wiki';
4 | import 'material-design-icons/iconfont/material-icons.css';
5 | import Header from './header';
6 |
7 | const User = ( { user, side, wiki } ) => {
8 | if ( !user ) {
9 | return (
10 |
11 | );
12 | }
13 |
14 | let href;
15 | if ( wiki ) {
16 | href = `https://${wiki.domain}/wiki/User:${user.replace( / /g, '_' )}`;
17 | }
18 |
19 | return (
20 |
21 | person
22 | {user}
23 |
24 | );
25 | };
26 |
27 | User.propTypes = {
28 | user: PropTypes.string,
29 | side: PropTypes.string,
30 | wiki: PropTypes.instanceOf( Wiki )
31 | };
32 |
33 | User.defaultProps = {
34 | user: undefined,
35 | side: undefined,
36 | wiki: undefined
37 | };
38 |
39 | export default User;
40 |
--------------------------------------------------------------------------------
/client/src/components/timeline/date-list.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { OrderedMap } from 'immutable';
4 | import Date from './date';
5 | import RevisionList from './revision-list';
6 |
7 | const DateList = ( { revisions } ) => {
8 | let prev;
9 |
10 | return revisions.map( ( list, date ) => {
11 | let last = prev ? prev.last() : undefined;
12 |
13 | prev = list;
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | } ).toArray();
25 | };
26 |
27 | DateList.propTypes = {
28 | revisions: PropTypes.instanceOf( OrderedMap ).isRequired
29 | };
30 |
31 | export default DateList;
32 |
--------------------------------------------------------------------------------
/i18n/zgh.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Aslmad mohamed belarhzali",
5 | "Hakim1bal"
6 | ]
7 | },
8 | "app-feedback-link": "ⵜⵉⵡⵉⵙⵉ",
9 | "between-edits": "$1 ⴳⵔ ⵙⵉⵏ ⵏ ⵉⵙⵏⴼⵍⵏ",
10 | "error": "ⴰⵣⴳⴰⵍ",
11 | "error-help": "$1, ⵉⵍⵎⵎⴰ $2.",
12 | "error-help-report": "ⵛⴽⴽⵎ ⵙ ⵓⵣⴳⴰⵍ",
13 | "error-help-refresh": "ⵙⵙⵜⵔⴻⵔ ⵜⴰⵙⵏⴰ",
14 | "error-message-request-url": "ⵜⵓⵜⵜⵔⴰ ⵏ URL",
15 | "field-label-end-date": "ⴰⵙⴰⴽⵓⴷ ⵏ ⵜⴳⵉⵔⴰ",
16 | "field-label-start-date": "ⴰⵙⴰⴽⵓⴷ ⵏ ⵓⵏⵜⵜⵉ",
17 | "field-label-users": "ⵉⵏⵙⵙⵎⵔⴰⵙ",
18 | "field-label-wiki": "ⵡⵉⴽⵉ",
19 | "field-select-no-results": "ⵓⵔ ⵜⴻⵜⵜⵢⴰⴼⴰ ⴰⵡⴷ ⵢⴰⵜ ⵜⵢⴰⴼⵓⵜ",
20 | "field-select-search-prompt": "ⴰⵔⴰ ⴰⴼⴰⴷ ⴰⴷ ⵜⵔⵣⵓⴷ",
21 | "made-by": "ⵜⵙⴽⵔ ⵜ ⵜⵎⵔⵙⵍⵜ ⵡⵉⴽⵉⵎⵉⴷⵢⴰ $1",
22 | "privacy-policy": "ⵜⴰⵙⵔⵜⵉⵜ ⵏ ⵜⵉⵏⵏⵓⵜⵍⴰ",
23 | "view-source-code-on": "ⵙⴽⵏ ⴳ $1",
24 | "warning-no-results": "ⵓⵔ ⵍⵍⵉⵏⵜ ⵜⵢⴰⴼⵓⵜⵉⵏ",
25 | "discuss-on-wiki": "ⴱⴹⵓ ⵜⵉⵢⴰⴼⵓⵜⵉⵏ ⴰⴷ",
26 | "discuss-on-wiki-copy": "ⵙⵙⵏⵖⵍ",
27 | "discuss-on-wiki-copied": "ⵉⵜⵜⵓⵏⵖⵍ!"
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/actions/query.js:
--------------------------------------------------------------------------------
1 | export const EVENTS = [
2 | 'QUERY_UPDATE',
3 | 'QUERY_USER_CHANGE',
4 | 'QUERY_WIKI_CHANGE',
5 | 'QUERY_START_DATE_CHANGE',
6 | 'QUERY_END_DATE_CHANGE'
7 | ];
8 |
9 | // Setting the default query is not a Query event that should be listened to.
10 | export function setDefaultQuery( query ) {
11 | return {
12 | type: 'QUERY_SET_DEFAULT',
13 | query
14 | };
15 | }
16 |
17 | export function updateQuery( query ) {
18 | return {
19 | type: 'QUERY_UPDATE',
20 | query
21 | };
22 | }
23 |
24 | export function userChange( users ) {
25 | return {
26 | type: 'QUERY_USER_CHANGE',
27 | users
28 | };
29 | }
30 |
31 | export function wikiChange( wiki ) {
32 | return {
33 | type: 'QUERY_WIKI_CHANGE',
34 | wiki
35 | };
36 | }
37 |
38 | export function startDateChange( startDate ) {
39 | return {
40 | type: 'QUERY_START_DATE_CHANGE',
41 | startDate
42 | };
43 | }
44 |
45 | export function endDateChange( endDate ) {
46 | return {
47 | type: 'QUERY_END_DATE_CHANGE',
48 | endDate
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/client/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= htmlWebpackPlugin.options.title %>
5 |
6 |
7 |
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | This tool requires JavaScript to function.
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/client/src/components/timeline/header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Link from 'app/components/link';
4 |
5 | const Header = ( { children, href, className, side } ) => {
6 | if ( !children ) {
7 | return (
8 |
9 | );
10 | }
11 |
12 | className = [
13 | ...className.split( ' ' ),
14 | 'user',
15 | 'col-6',
16 | 'pt-2',
17 | 'pb-2',
18 | 'd-flex',
19 | 'justify-content-center',
20 | 'align-items-center'
21 | ];
22 |
23 | if ( side ) {
24 | className = [
25 | ...className,
26 | side
27 | ];
28 | }
29 |
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | };
36 |
37 | Header.propTypes = {
38 | children: PropTypes.node,
39 | side: PropTypes.string,
40 | className: PropTypes.string,
41 | href: PropTypes.string
42 | };
43 |
44 | Header.defaultProps = {
45 | children: undefined,
46 | side: undefined,
47 | className: '',
48 | href: undefined
49 | };
50 |
51 | export default Header;
52 |
--------------------------------------------------------------------------------
/server/src/AppErrorHandler.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
21 | }
22 |
23 | /**
24 | * @param ServerRequestInterface $request
25 | * @param ResponseInterface $response
26 | * @param \Exception $e
27 | * @return ResponseInterface $response
28 | */
29 | public function __invoke(
30 | ServerRequestInterface $request, ResponseInterface $response, \Exception $e
31 | ) {
32 | $errorCode = $e->getCode() ?: 400;
33 | $message = $e->getMessage();
34 |
35 | // maybe we shouldn't log everything
36 | $this->logger->error( $message, [ $e->getTraceAsString() ] );
37 |
38 | return $response
39 | ->withStatus( $errorCode )
40 | ->withHeader( 'Content-Type', 'application/json' )
41 | ->withJson( [ 'message' => $message ] );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/server/src/Middleware/ConnectionManagerMiddleware.php:
--------------------------------------------------------------------------------
1 | connService = $connService;
20 | }
21 |
22 | /**
23 | * Reads the project name Ex. enwiki from the route
24 | * and creates a connection to the corresponding HOST/DB
25 | *
26 | * @param ServerRequestInterface $request
27 | * @param ResponseInterface $response
28 | * @param callable $next
29 | * @return ResponseInterface
30 | */
31 | public function __invoke(
32 | ServerRequestInterface $request, ResponseInterface $response, callable $next
33 | ) {
34 | $projectName = $request->getAttribute( 'route' )->getArgument( 'project' );
35 | $this->connService->connect( $projectName );
36 |
37 | $response = $next( $request, $response );
38 |
39 | return $response;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/client/src/components/form.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SelectUsersContainer from 'app/components/fields/select-users.container';
3 | import SelectWikiContainer from 'app/components/fields/select-wiki.container';
4 | import DateRangeContainer from 'app/components/fields/date-range.container';
5 | import { Message } from '@wikimedia/react.i18n';
6 |
7 | const Form = () => (
8 |
29 | );
30 |
31 | export default Form;
32 |
--------------------------------------------------------------------------------
/client/src/components/share/share.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { Button, Modal } from 'reactstrap';
4 | import Share from './share';
5 |
6 | test( 'hide button when no results are present', () => {
7 | const share = shallow( );
8 | expect( share.find( Button ).first().hasClass( 'invisible' ) ).toBeTruthy();
9 | } );
10 |
11 | test( 'show button when results are present', () => {
12 | const share = shallow( );
13 | expect( share.find( Button ).first().hasClass( 'visible' ) ).toBeTruthy();
14 | } );
15 |
16 | test( 'clicking on the button opens the modal with selected text', () => {
17 | const share = shallow( );
18 |
19 | expect( share.find( Modal ).prop( 'isOpen' ) ).toBeFalsy();
20 |
21 | share.find( Button ).first().simulate( 'click' );
22 | expect( share.find( Modal ).prop( 'isOpen' ) ).toBeTruthy();
23 | } );
24 |
--------------------------------------------------------------------------------
/client/src/components/timeline/revision-list.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import moment from 'moment';
4 | import { OrderedMap } from 'immutable';
5 | import Revision from 'app/entities/revision';
6 | import RevisionContainer from './revision/revision.container';
7 |
8 | const RevisionList = ( { revisions, last } ) => {
9 | let prev = last;
10 |
11 | return revisions.map( ( revision ) => {
12 | let duration;
13 | let samePage = false;
14 |
15 | // If we are switching sides, show the duraction.
16 | if ( prev && prev.user !== revision.user ) {
17 | duration = moment.duration( prev.timestamp.diff( revision.timestamp ) );
18 | samePage = prev.pageId === revision.pageId;
19 | }
20 |
21 | // Set the previous state for
22 | prev = revision;
23 |
24 | return (
25 |
26 | );
27 | } ).toArray();
28 | };
29 |
30 | RevisionList.propTypes = {
31 | revisions: PropTypes.instanceOf( OrderedMap ).isRequired,
32 | last: PropTypes.instanceOf( Revision )
33 | };
34 |
35 | RevisionList.defaultProps = {
36 | last: undefined
37 | };
38 |
39 | export default RevisionList;
40 |
--------------------------------------------------------------------------------
/i18n/ne.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Nirajan pant",
5 | "बडा काजी",
6 | "सरोज कुमार ढकाल"
7 | ]
8 | },
9 | "app-feedback-link": "सहायता",
10 | "app-title": "अन्तरकृया समयरेखा",
11 | "back-top": "सुरुमा जानुहोस्",
12 | "between-edits": "$1 सम्पादनको बीचमा",
13 | "between-interactions": "$1 अन्तरकृयाको बीचमा",
14 | "error": "त्रुटी",
15 | "error-help": "$1 र त्यसपछि $2",
16 | "error-help-report": "त्रुटी उजुरी गर्नुहोस्",
17 | "error-help-refresh": "पृष्ठ ताजा पार्नुहोस्",
18 | "error-message-request-url": "अनुरोध URL",
19 | "field-label-end-date": "समाप्ति मिति",
20 | "field-label-start-date": "आरम्भ मिति",
21 | "field-label-users": "प्रयोगकर्ताहरू",
22 | "field-label-wiki": "विकि",
23 | "field-select-placeholder": "छान्नुहोस्...",
24 | "field-select-no-results": "कुनै पनि नतिजाहरू भेटिएनन्",
25 | "privacy-policy": "गोपनीयता नीति",
26 | "powered-by": "विकिमिडिया टुलफर्जले प्रबर्धन गरेको",
27 | "report-bug": "बग उजुरी गर्ने",
28 | "revision-edit-summary-removed": "सम्पादन सारांश हटाइयो",
29 | "view-source-code-on": "$1 मा हेर्नुहोस्",
30 | "warning-no-results": "कुनै नतिजाहरू छैनन्",
31 | "discuss-on-wiki-copy": "प्रतिलिपि गर्नुहोस्",
32 | "discuss-on-wiki-copied": "प्रतिलिपी गरियो !"
33 | }
34 |
--------------------------------------------------------------------------------
/i18n/bg.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "StanProg",
5 | "Vlad5250"
6 | ]
7 | },
8 | "app-feedback-link": "Помощ",
9 | "app-title": "Хронология на взаимодействието",
10 | "back-top": "Връщане в началото",
11 | "between-edits": "$1 между редакциите",
12 | "between-interactions": "$1 между взаимодействията",
13 | "error": "Грешка",
14 | "error-help": "$1, след това $2.",
15 | "error-help-report": "Съобщаване за грешката",
16 | "error-help-refresh": "презареждане на страницата",
17 | "error-message-request-url": "URL на заявката:",
18 | "licensed-under": "Под лиценза $1",
19 | "field-label-end-date": "Крайна дата",
20 | "field-label-start-date": "Начална дата",
21 | "field-label-users": "Потребители",
22 | "field-label-wiki": "Уики",
23 | "field-select-placeholder": "Изберете...",
24 | "field-select-no-results": "Не бяха открити резултати",
25 | "privacy-policy": "Защита на личните данни",
26 | "report-bug": "Съобщаване за грешка",
27 | "revision-edit-summary-removed": "премахнато резюмето на редакцията",
28 | "view-source-code-on": "Преглед в $1",
29 | "warning-no-results": "Няма резултати",
30 | "discuss-on-wiki": "Споделяне на резултатите",
31 | "discuss-on-wiki-copy": "Копиране",
32 | "discuss-on-wiki-copied": "Копирано!"
33 | }
34 |
--------------------------------------------------------------------------------
/i18n/my.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Dr Lotus Black"
5 | ]
6 | },
7 | "app-feedback-link": "အကူအညီ",
8 | "back-top": "ထိပ်ဆုံးသို့ ပြန်သွားရန်",
9 | "between-edits": "$1 ကြား တည်းဖြတ်မှုများ",
10 | "error": "အမှား",
11 | "error-help": "$1၊ ပြီးလျင် $2။",
12 | "error-help-report": "အမှားကို အစီရင်ခံရန်",
13 | "error-help-refresh": "စာမျက်နှာကို ပြန်လည်ဆန်းသစ်ရန်",
14 | "error-message-request-url": "URL တောင်းဆိုရန်",
15 | "licensed-under": "$1 အောက်ရှိ လိုင်စင်သတ်မှတ်ထားသည်",
16 | "field-label-end-date": "ကုန်ဆုံးသည့်ရက်စွဲ",
17 | "field-label-start-date": "စတင်သည့်ရက်စွဲ",
18 | "field-label-users": "အသုံးပြုသူများ",
19 | "field-label-wiki": "ဝီကီ",
20 | "field-select-placeholder": "ရွေးချယ်...",
21 | "field-select-no-results": "မည်သည့်ရလဒ်မျှ မရပါ",
22 | "field-select-search-prompt": "ရှာဖွေရန် ရိုက်ထည့်ပါ",
23 | "made-by": "ဝီကီမီဒီယာ ဖောင်ဒေးရှင်း၏ $1 ဖြင့် ပြုလုပ်သည်။",
24 | "made-by-team": "အနှာက်ယှက်ပေးခြင်းကို တန်ပြန်တုန့်ပြန်သည့် ကိရိယာများအဖွဲ့",
25 | "privacy-policy": "ကိုယ်ရေးကိုယ်တာ မူဝါဒ",
26 | "revision-edit-summary-removed": "အကျဉ်းချုပ်တည်းဖြတ်မှုကို ဖယ်ရှားလိုက်သည်",
27 | "view-source-code-on": "$1 ပေါ်တွင် ကြည့်ရှုရန်",
28 | "warning-no-results": "ရလဒ်များ မရှိပါ",
29 | "discuss-on-wiki": "ဤရလဒ်မျာကို မျှဝေရန်",
30 | "discuss-on-wiki-copy": "မိတ္တူကူး",
31 | "discuss-on-wiki-copied": "မိတ္တူကူးလိုက်ပြီ"
32 | }
33 |
--------------------------------------------------------------------------------
/i18n/bn.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Aftabuzzaman",
5 | "CX Zoom",
6 | "আফতাবুজ্জামান"
7 | ]
8 | },
9 | "app-description": "দুইজন ব্যবহারকারীর জন্য সময়ানুক্রমিক ইতিহাস যেখানে দুইজনেই সম্পাদনা করেছেন",
10 | "app-feedback-link": "সাহায্য",
11 | "back-top": "উপরে ফিরুন",
12 | "between-edits": "সম্পাদনার মধ্যে সময় $1",
13 | "error": "ত্রুটি",
14 | "error-help": "$1, এবং তারপর $2।",
15 | "error-help-report": "ত্রুটি প্রতিবেদন করুন",
16 | "error-help-refresh": "পাতাটি পুনঃসতেজ করুন",
17 | "error-message-request-url": "অনুরোধের URL",
18 | "info-no-users": "শুরু করতে দুইজন ব্যবহারকারী প্রদান করুন।",
19 | "info-no-wiki": "শুরু করতে একটি উইকি প্রদান করুন।",
20 | "info-required-fields": "শুরু করতে দুইজন ব্যবহারকারী ও একটি উইকি প্রদান করুন।",
21 | "licensed-under": "$1-এর অধীনে লাইসেন্সকৃত",
22 | "field-label-end-date": "শেষের তারিখ",
23 | "field-label-start-date": "শুরুর তারিখ",
24 | "field-label-users": "ব্যবহারকারী",
25 | "field-label-wiki": "উইকি",
26 | "field-select-placeholder": "নির্বাচন করুন...",
27 | "field-select-no-results": "কোনো ফলাফল পাওয়া যায়নি",
28 | "field-select-search-prompt": "অনুসন্ধান করতে লিখুন",
29 | "privacy-policy": "গোপনীয়তার নীতি",
30 | "report-bug": "একটি সমস্যা প্রতিবেদন করুন",
31 | "revision-edit-summary-removed": "সম্পাদনা সারাংশ অপসারিত হয়েছে",
32 | "view-source-code-on": "$1-এ দেখুন",
33 | "warning-no-results": "কোন ফলাফল নেই",
34 | "discuss-on-wiki-copy": "অনুলিপি করুন",
35 | "discuss-on-wiki-copied": "অনুলিপি হয়েছে!"
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/components/timeline/status.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Message } from '@wikimedia/react.i18n';
4 | import Alert from './alert';
5 | import Spinner from './spinner';
6 | import ErrorMessageContainer from './error-message.container';
7 |
8 | const Status = ( { status } ) => {
9 | let message;
10 |
11 | switch ( status ) {
12 | case 'fetching':
13 | message = ;
14 | break;
15 | case 'notready':
16 | message = (
17 |
18 |
19 |
20 | );
21 | break;
22 | case 'nowiki':
23 | message = (
24 |
25 |
26 |
27 | );
28 | break;
29 | case 'nousers':
30 | message = (
31 |
32 |
33 |
34 | );
35 | break;
36 | case 'noresults':
37 | message = (
38 |
39 |
40 |
41 | );
42 | break;
43 | case 'error':
44 | message = ;
45 | break;
46 | default:
47 | return null;
48 | }
49 |
50 | return (
51 |
52 |
53 | {message}
54 |
55 |
56 | );
57 | };
58 |
59 | Status.propTypes = {
60 | status: PropTypes.string.isRequired
61 | };
62 |
63 | export default Status;
64 |
--------------------------------------------------------------------------------
/client/src/selectors/diff.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import Revision from 'app/entities/revision';
3 | import { makeGetTitle } from './revisions';
4 | import { getWiki } from './wiki';
5 |
6 | const getRevision = ( revisions, id ) => {
7 | if ( !id ) {
8 | return;
9 | }
10 |
11 | return revisions.get( id, new Revision( {
12 | id,
13 | meta: {
14 | status: 'ready'
15 | }
16 | } ) );
17 | };
18 |
19 | export const makeGetFromRevision = () => (
20 | createSelector(
21 | state => state.revisions.list,
22 | ( _, props ) => props.diff.fromrevid,
23 | getRevision
24 | )
25 | );
26 |
27 | export const makeGetToRevision = () => (
28 | createSelector(
29 | state => state.revisions.list,
30 | ( _, props ) => props.diff.torevid,
31 | getRevision
32 | )
33 | );
34 |
35 | export const makeGetDiff = () => (
36 | createSelector(
37 | state => state.diffs,
38 | ( _, props ) => props.revision,
39 | ( diffs, revision ) => (
40 | diffs.get( revision.id )
41 | )
42 | )
43 | );
44 |
45 | export const makeGetDiffUrl = () => {
46 | const getTitle = makeGetTitle();
47 |
48 | return createSelector(
49 | getWiki,
50 | getTitle,
51 | ( _, props ) => {
52 | if ( props.revision ) {
53 | return props.revision.id;
54 | }
55 |
56 | return;
57 | },
58 | ( wiki, title, id ) => {
59 | if ( !wiki || !id || !title ) {
60 | return;
61 | }
62 |
63 | return `https://${wiki.domain}/wiki/${title.replace( / /g, '_' )}?diff=prev&oldid=${id}`;
64 | }
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/etc/lighttpd/lighttpd.conf:
--------------------------------------------------------------------------------
1 | server.modules = (
2 | "mod_access",
3 | "mod_alias",
4 | "mod_compress",
5 | "mod_redirect",
6 | "mod_fastcgi",
7 | "mod_rewrite",
8 | )
9 |
10 | server.document-root = "/var/www/html"
11 | server.upload-dirs = ( "/var/cache/lighttpd/uploads" )
12 | server.errorlog = "/var/log/lighttpd/error.log"
13 | server.pid-file = "/var/run/lighttpd.pid"
14 | server.username = "www-data"
15 | server.groupname = "www-data"
16 | server.port = 80
17 |
18 | index-file.names = ( "index.php", "index.html", "index.lighttpd.html" )
19 | url.access-deny = ( "~", ".inc" )
20 | static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
21 |
22 | compress.cache-dir = "/var/cache/lighttpd/compress/"
23 | compress.filetype = ( "application/javascript", "text/css", "text/html", "text/plain" )
24 |
25 | # default listening port for IPv6 falls back to the IPv4 port
26 | include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
27 | include_shell "/usr/share/lighttpd/create-mime.assign.pl"
28 | include_shell "/usr/share/lighttpd/include-conf-enabled.pl"
29 |
30 | # fastcgi
31 | fastcgi.server = ( ".php" => ((
32 | "bin-path" => "/usr/bin/php-cgi",
33 | "socket" => "/tmp/php.socket"
34 | )))
35 |
36 | # rewrite everything to index.html unless api is requested
37 | url.rewrite-if-not-file = (
38 | "^/api/(.*)" => "/api/index.php/$0",
39 | "^/.*$" => "/index.html"
40 | )
41 |
--------------------------------------------------------------------------------
/client/src/reducers/revisions/list.js:
--------------------------------------------------------------------------------
1 | import { OrderedMap } from 'immutable';
2 |
3 | export default ( state = new OrderedMap(), action ) => {
4 | switch ( action.type ) {
5 | case 'REVISIONS_ADD':
6 | case 'REVISIONS_SINGLE_ADD':
7 | return state
8 | .merge( action.revisions )
9 | // Since the ids are all from the same wiki, they are in a guaranteed
10 | // order from oldest to newest.
11 | .sortBy( revision => revision.id );
12 | case 'REVISIONS_SINGLE_STATUS_SET':
13 | if ( state.has( action.id ) ) {
14 | return state.setIn( [ action.id, 'meta', 'status' ], action.status );
15 | }
16 |
17 | return state;
18 | case 'REVISIONS_SET':
19 | return action.revisions.sortBy( revision => revision.id );
20 | case 'QUERY_USER_CHANGE':
21 | case 'QUERY_WIKI_CHANGE':
22 | case 'QUERY_START_DATE_CHANGE':
23 | case 'QUERY_END_DATE_CHANGE':
24 | return new OrderedMap();
25 | case 'DIFFS_SHOW_SET':
26 | if ( action.show === false && action.diff.fromrevid && state.has( action.diff.fromrevid ) ) {
27 | // If the diff was without error, ensure that the from revision is not in
28 | // error, if it is, remove it.
29 | if ( state.get( action.diff.fromrevid ).meta.status === 'error' ) {
30 | state = state.remove( action.diff.fromrevid );
31 | }
32 | }
33 |
34 | return state;
35 | case 'REVISIONS_SINGLE_ERROR':
36 | return state
37 | .setIn( [ action.id, 'meta', 'status' ], 'error' )
38 | .setIn( [ action.id, 'meta', 'error' ], action.error );
39 | default:
40 | return state;
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/i18n/ckb.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Arya sarhan",
5 | "Épine"
6 | ]
7 | },
8 | "app-feedback-link": "یارمەتی",
9 | "back-top": "گەڕانەوە بۆ سەرەوە",
10 | "between-edits": "$1 لە نێوان دەستکارییەکان",
11 | "error": "ھەڵە",
12 | "error-help": "$1، دواتریش $2.",
13 | "error-help-report": "سکاڵای کێشەیەک بدە",
14 | "error-help-refresh": "پەڕەکە خاوێن بکەرەوە",
15 | "error-message-request-url": "داوای ناونیشان بکە",
16 | "error-suppressed-diff": "ناتوانیت ئەم جیاوازییە ببینیت چوونکە یەکێک یان ھەردووک بەسەرداچوونەوەکان خامۆشکراون.",
17 | "info-no-users": "سەرەتا دوو بەکارھێنەر بدە.",
18 | "info-no-wiki": "سەرەتا ویکییەک بدە.",
19 | "info-required-fields": "سەرەتا دوو بەکارھێنەر و ویکییەک بدە.",
20 | "licensed-under": "مۆڵەتنامە لەژێر $1دایە",
21 | "field-label-end-date": "ڕێکەوتی کۆتایی",
22 | "field-label-start-date": "ڕێکەوتی دەستپێکردن",
23 | "field-label-users": "بەکارھێنەران",
24 | "field-label-wiki": "ویکی",
25 | "field-select-placeholder": "ھەڵبژاردن...",
26 | "field-select-no-results": "ھیچ ئەنجامێک نەدۆزرایەوە",
27 | "field-select-search-prompt": "بۆ گەڕان لێرە بنووسە",
28 | "made-by": "لەلایەن $1ی دامەزراوەی ویکیمیدیاوە دروست کراوە.",
29 | "made-by-team": "تیمی ئامرازی دژە-ھەراسانکاری",
30 | "privacy-policy": "سیاسەتی تایبەتێتی",
31 | "report-bug": "سکاڵای کێشەیەک بدە",
32 | "revision-edit-summary-removed": "کورتەی دەستکاری لابرا",
33 | "view-source-code-on": "لە $1دا بیبینە",
34 | "warning-no-results": "هیچ ئەنجامێک نییە",
35 | "discuss-on-wiki-copy": "لەبەرگرتنەوە",
36 | "discuss-on-wiki-copied": "لەبەرگیرایەوە"
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/components/timeline/error-message.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { AjaxError } from 'rxjs';
4 | import { Message } from '@wikimedia/react.i18n';
5 | import Alert from './alert';
6 |
7 | const ErrorMessage = ( { error, clearError } ) => {
8 | let details;
9 | let clearButton;
10 | let code;
11 |
12 | if ( error instanceof AjaxError ) {
13 | if ( error.response && error.response.error && error.response.error.code ) {
14 | code = error.response.error.code;
15 | } else if ( error.status ) {
16 | code = error.status;
17 | }
18 |
19 | details = (
20 |
29 | );
30 | }
31 |
32 | if ( clearError ) {
33 | clearButton = (
34 | clearError()}>
35 | ×
36 |
37 | );
38 | }
39 |
40 | return (
41 |
42 | {clearButton}
43 | {code}
44 | {error.message}
45 | {details}
46 |
47 | );
48 | };
49 |
50 | ErrorMessage.propTypes = {
51 | error: PropTypes.instanceOf( Error ).isRequired,
52 | clearError: PropTypes.func
53 | };
54 |
55 | ErrorMessage.defaultProps = {
56 | clearError: null
57 | };
58 |
59 | export default ErrorMessage;
60 |
--------------------------------------------------------------------------------
/i18n/kab.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Belkacem77",
5 | "ButterflyOfFire"
6 | ]
7 | },
8 | "app-description": "Azray n sin n yiseqdacen i icerken isebtar ideg beddlen.",
9 | "app-feedback-link": "Tallelt",
10 | "app-title": "Awitay n temyigawt",
11 | "back-top": "Uɣal d asawen",
12 | "between-edits": "$1 gar sin n yibeddilen",
13 | "between-interactions": "$1 gar temyigawin",
14 | "error": "Tuccḍa",
15 | "error-help": "$1 sakin $2.",
16 | "error-help-report": "Azen tuccḍa",
17 | "error-help-refresh": "smiren asebter",
18 | "error-message-request-url": "Tansa URL n tuttra",
19 | "info-no-users": "Mudd-d sin n yiseqdacen akken ad tebduḍ",
20 | "info-no-wiki": "Mudd-d awiki akken ad tebduḍ.",
21 | "info-required-fields": "Mudd-d sin n yiseqdacen akked uwiki akken ad tebduḍ.",
22 | "licensed-under": "Ddaw n turagt $1",
23 | "field-label-end-date": "Azemz n taggara",
24 | "field-label-start-date": "Azemz n tazwara",
25 | "field-label-users": "Iseqdacen",
26 | "field-label-wiki": "Awiki",
27 | "field-select-placeholder": "Fren...",
28 | "field-select-no-results": "Ulac igmaḍ",
29 | "field-select-search-prompt": "Aru akken ad tnadiḍ",
30 | "made-by": "Ixdem-it $1 seg tesbeddit Wikimedia.",
31 | "privacy-policy": "Tasertit n tbaḍnit",
32 | "powered-by": "Yessul-it Wikimedia Toolforge",
33 | "report-bug": "Azen abug",
34 | "revision-edit-summary-removed": "agzul n yibeddilen yettwakkes",
35 | "view-source-code-on": "Wali-t deg $1",
36 | "warning-no-results": "Ulac igmad",
37 | "discuss-on-wiki-copy": "Nɣel",
38 | "discuss-on-wiki-copied": "Yettwanɣel!"
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/utils/ip-validator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NOTE: Borrows from https://github.com/wikimedia/mediawiki/blob/master/includes/libs/IP.php
3 | */
4 |
5 | export const isIPv4Address = ( address ) => {
6 |
7 | if ( typeof address !== 'string' ) {
8 | return false;
9 | }
10 |
11 | let RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])';
12 | let RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE;
13 |
14 | return ( new RegExp( '^' + RE_IP_ADD + '$' ).test( address ) );
15 | };
16 |
17 | export const isIPv6Address = ( address ) => {
18 | let RE_IPV6_ADD;
19 |
20 | if ( typeof address !== 'string' ) {
21 | return false;
22 | }
23 |
24 | RE_IPV6_ADD =
25 | '(?:' + // starts with "::" (including "::")
26 | ':(?::|(?::' +
27 | '[0-9A-Fa-f]{1,4}' +
28 | '){1,7})' +
29 | '|' + // ends with "::" (except "::")
30 | '[0-9A-Fa-f]{1,4}' +
31 | '(?::' +
32 | '[0-9A-Fa-f]{1,4}' +
33 | '){0,6}::' +
34 | '|' + // contains no "::"
35 | '[0-9A-Fa-f]{1,4}' +
36 | '(?::' +
37 | '[0-9A-Fa-f]{1,4}' +
38 | '){7}' +
39 | ')';
40 |
41 | if ( new RegExp( '^' + RE_IPV6_ADD + '$' ).test( address ) ) {
42 | return true;
43 | }
44 |
45 | // contains one "::" in the middle (single '::' check below)
46 | RE_IPV6_ADD =
47 | '[0-9A-Fa-f]{1,4}' +
48 | '(?:::?' +
49 | '[0-9A-Fa-f]{1,4}' +
50 | '){1,6}';
51 |
52 | return (
53 | new RegExp( '^' + RE_IPV6_ADD + '$' ).test( address ) &&
54 | /::/.test( address ) &&
55 | !/::.*::/.test( address )
56 | );
57 | };
58 |
59 | export const isIPAddress = ( address ) => {
60 | return isIPv4Address( address ) ||
61 | isIPv6Address( address );
62 | };
63 |
--------------------------------------------------------------------------------
/i18n/bcl.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Brazal.dang",
5 | "Daramlagon"
6 | ]
7 | },
8 | "app-feedback-link": "Tabang",
9 | "app-title": "Timeline kan interaksyon",
10 | "back-top": "Magbalik sa itaas",
11 | "between-edits": "$1 sa pagitan kan kaliwatan",
12 | "between-interactions": "$1 sa pagitan kan interaksyon",
13 | "error": "Salâ",
14 | "error-help": "$1, asin $2.",
15 | "error-help-report": "Ireport an sala",
16 | "error-help-refresh": "I-refresh an pahina",
17 | "error-message-request-url": "Hagad URL",
18 | "info-no-users": "Magtao nin duwang paragamit para makapagpoon.",
19 | "info-no-wiki": "Magtao nin wiki para makapagpoon.",
20 | "info-required-fields": "Magtao nin duwang paragamit asin wiki para makapagpoon.",
21 | "licensed-under": "Lisensiya sa $1",
22 | "field-label-end-date": "Petsa kan pagtapos",
23 | "field-label-start-date": "Petsa sa pagpoon",
24 | "field-label-users": "Mga Paragamit",
25 | "field-label-wiki": "Wiki",
26 | "field-select-placeholder": "Magpili...",
27 | "field-select-no-results": "Mayong mga resultang nanumpungan",
28 | "made-by": "Ginibo kan Wikimedia Foundation's $1.",
29 | "made-by-team": "Grupo kan Anti-Harassment Tools",
30 | "privacy-policy": "Kalakawan nin pribasidad",
31 | "powered-by": "Pinapagana kan Wikimedia Toolforge",
32 | "report-bug": "Magreport nin bug",
33 | "revision-edit-summary-removed": "pagliwat na sumaryo pinaghale",
34 | "view-source-code-on": "Hilingon sa $1",
35 | "warning-no-results": "Mayo nin mga resulta",
36 | "discuss-on-wiki": "Ibahagi an mga resulta",
37 | "discuss-on-wiki-copy": "Kopyahon",
38 | "discuss-on-wiki-copied": "Matrayumpong pinagkopya"
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/reducers/diffs.js:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 | import Diff from 'app/entities/diff';
3 |
4 | export default ( state = new Map(), action ) => {
5 | switch ( action.type ) {
6 | case 'REVISIONS_ADD':
7 | case 'REVISIONS_SINGLE_ADD':
8 | return action.revisions.reduce( ( map, revision ) => {
9 | if ( map.has( revision.id ) ) {
10 | return map;
11 | }
12 |
13 | return map.set( revision.id, new Diff( {
14 | torevid: revision.id,
15 | touser: revision.user
16 | } ) );
17 | }, state );
18 | case 'DIFFS_SET':
19 | return state.set( action.diff.torevid, action.diff );
20 | case 'DIFFS_SHOW_SET':
21 | if ( action.show === false && action.diff.meta.status === 'error' ) {
22 | // If user is hiding the diff, remove the error. This way the request
23 | // will be executed the next time the user requests the diff.
24 | state = state.set( action.diff.torevid,
25 | action.diff.setIn( [ 'meta', 'status' ], 'ready' )
26 | .setIn( [ 'meta', 'error' ], undefined )
27 | );
28 | }
29 |
30 | return state.setIn( [ action.diff.torevid, 'meta', 'show' ], action.show );
31 | case 'DIFFS_STATUS_SET':
32 | return state.setIn( [ action.diff.torevid, 'meta', 'status' ], action.status );
33 | case 'DIFFS_THROW_ERROR':
34 | return state
35 | .setIn( [ action.diff.torevid, 'meta', 'status' ], 'error' )
36 | .setIn( [ action.diff.torevid, 'meta', 'error' ], action.error );
37 | case 'QUERY_WIKI_CHANGE':
38 | return new Map();
39 | case 'QUERY_USER_CHANGE':
40 | return state.filter( diff => {
41 | return action.users.includes( diff.touser );
42 | } );
43 | default:
44 | return state;
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/client/src/actions/revisions.js:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 |
3 | export function fetchRevisions() {
4 | return {
5 | type: 'REVISIONS_FETCH'
6 | };
7 | }
8 |
9 | export function fetchRevision( id ) {
10 | return {
11 | type: 'REVISIONS_SINGLE_FETCH',
12 | id
13 | };
14 | }
15 |
16 | export function setRevisionStatus( id, status ) {
17 | return {
18 | type: 'REVISIONS_SINGLE_STATUS_SET',
19 | id,
20 | status
21 | };
22 | }
23 |
24 | export function addRevision( revision ) {
25 | return {
26 | type: 'REVISIONS_SINGLE_ADD',
27 | // We only use the array syntax or the id will be converted to a string.
28 | revisions: new Map( [
29 | [
30 | revision.id,
31 | revision
32 | ]
33 | ] )
34 | };
35 | }
36 |
37 | export function setStatusFetching() {
38 | return {
39 | type: 'REVISIONS_FETCHING'
40 | };
41 | }
42 |
43 | export function setStatusReady() {
44 | return {
45 | type: 'REVISIONS_READY'
46 | };
47 | }
48 |
49 | export function setStatusNotReady() {
50 | return {
51 | type: 'REVISIONS_NOT_READY'
52 | };
53 | }
54 |
55 | export function setStatusDone() {
56 | return {
57 | type: 'REVISIONS_DONE'
58 | };
59 | }
60 |
61 | export function addRevisions( revisions, cont = '' ) {
62 | return {
63 | type: 'REVISIONS_ADD',
64 | revisions,
65 | cont
66 | };
67 | }
68 |
69 | export function throwError( error ) {
70 | return {
71 | type: 'REVISIONS_ERROR',
72 | error
73 | };
74 | }
75 |
76 | export function throwRevisionError( id, error ) {
77 | return {
78 | type: 'REVISIONS_SINGLE_ERROR',
79 | id,
80 | error
81 | };
82 | }
83 |
84 | export function clearError() {
85 | return {
86 | type: 'REVISIONS_ERROR_CLEAR'
87 | };
88 | }
89 |
--------------------------------------------------------------------------------
/server/src/Action/InteractionAction.php:
--------------------------------------------------------------------------------
1 | service = $service;
22 | }
23 |
24 | /**
25 | * @param Request $request
26 | * @param Response $response
27 | * @param array $args
28 | * @return Response
29 | */
30 | public function __invoke( Request $request, Response $response, $args ) {
31 | $users = $this->parseListQueryParam( $request->getQueryParam( 'user', null ) );
32 | $namespaces = $this->parseListQueryParam( $request->getQueryParam( 'namespace', null ) );
33 | $startDate = $request->getQueryParam( 'start_date', null );
34 | $endDate = $request->getQueryParam( 'end_date', null );
35 | $limit = $request->getQueryParam( 'limit', 50 );
36 | $continue = $request->getQueryParam( 'continue', null );
37 |
38 | list( $interaction, $continue ) =
39 | $this->service->getInteraction( $users, $namespaces, $startDate, $endDate, $limit, $continue );
40 |
41 | if ( $continue ) {
42 | $ret['continue'] = $continue;
43 | }
44 |
45 | $ret['data'] = $interaction;
46 |
47 | return $response->withJson( $ret );
48 | }
49 |
50 | /**
51 | * @param string $param
52 | * @return array
53 | */
54 | private function parseListQueryParam( $param ) {
55 | $params = [];
56 | if ( !is_null( $param ) ) {
57 | $params = explode( '|', $param );
58 | }
59 |
60 | return $params;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/i18n/zh-hans.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Deathkon",
5 | "Diskdance",
6 | "Liuxinyu970226",
7 | "Tranve"
8 | ]
9 | },
10 | "app-description": "两位用户在其均编辑过的页面上的历史,按时间顺序排列。",
11 | "app-feedback-link": "帮助",
12 | "app-title": "交互式时间线",
13 | "back-top": "返回顶部",
14 | "between-edits": "编辑间隔$1",
15 | "between-interactions": "$1之间交互",
16 | "error": "错误",
17 | "error-help": "$1,然后$2。",
18 | "error-help-report": "报告错误",
19 | "error-help-refresh": "刷新页面",
20 | "error-message-request-url": "请求URL",
21 | "error-suppressed-diff": "您不能查看此差异,因为修订版本之一(或两者)已被屏蔽。",
22 | "info-no-users": "请提供两位用户以开始。",
23 | "info-no-wiki": "请提供一个wiki以开始。",
24 | "info-required-fields": "请提供两位用户和wiki以开始。",
25 | "licensed-under": "采用$1授权",
26 | "field-label-end-date": "结束日期",
27 | "field-label-start-date": "开始日期",
28 | "field-label-users": "用户",
29 | "field-label-wiki": "Wiki",
30 | "field-select-placeholder": "选择...",
31 | "field-select-no-results": "找不到结果",
32 | "field-select-search-prompt": "搜索的类型",
33 | "made-by": "由维基媒体基金会的$1制作。",
34 | "made-by-team": "反骚扰工具团队",
35 | "privacy-policy": "隐私政策",
36 | "powered-by": "由维基媒体Toolforge提供支持",
37 | "report-bug": "报告错误",
38 | "revision-edit-summary-removed": "编辑摘要被移除",
39 | "view-source-code-on": "在$1查看",
40 | "warning-no-results": "没有结果",
41 | "discuss-on-wiki": "分享这些结果",
42 | "discuss-on-wiki-text": "在$1至$2之间,在$3上的[[User:$4]]([[User talk:$4|talk]] | [[Special:Contributions/$4|contribs]])与[[User:$5]]([[User talk:$5|talk]] | [[Special:Contributions/$5|contribs]])都对一些页面进行了编辑。您可以在[$6 Interaction Timeline]中查看按时间顺序列出的所有他们编辑的内容,或在[$7 Interaction Analyser]中以表格视图的方式查看。",
43 | "discuss-on-wiki-help": "将此wikitext复制并粘贴到on-wiki讨论中,以便与其他人共享这些交互式时间线的结果",
44 | "discuss-on-wiki-copy": "复制",
45 | "discuss-on-wiki-copied": "已复制!"
46 | }
47 |
--------------------------------------------------------------------------------
/client/src/selectors/revisions.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import { getWiki } from './wiki';
3 |
4 | export const makeGetTitle = () => (
5 | createSelector(
6 | state => getWiki( state ),
7 | ( _, props ) => {
8 | if ( props.revision ) {
9 | return props.revision.title;
10 | }
11 |
12 | return;
13 | },
14 | ( _, props ) => {
15 | if ( props.revision ) {
16 | return props.revision.pageNamespace;
17 | }
18 |
19 | return;
20 | },
21 | ( wiki, title, namespace ) => {
22 | if ( !title ) {
23 | return '';
24 | }
25 |
26 | if ( typeof namespace === 'undefined' ) {
27 | return title;
28 | }
29 |
30 | if ( namespace === 0 ) {
31 | return title;
32 | }
33 |
34 | if ( !wiki ) {
35 | return title;
36 | }
37 |
38 | if ( !wiki.namespaces.has( namespace ) ) {
39 | return title;
40 | }
41 |
42 | const name = wiki.namespaces.get( namespace ).name;
43 |
44 | return `${name}:${title}`;
45 | }
46 | )
47 | );
48 |
49 | export const makeGetRevisionUrl = () => {
50 | const getTitle = makeGetTitle();
51 |
52 | return createSelector(
53 | getWiki,
54 | getTitle,
55 | ( _, props ) => {
56 | if ( props.revision ) {
57 | return props.revision.id;
58 | }
59 |
60 | return;
61 | },
62 | ( wiki, title, id ) => {
63 | if ( !wiki || !id || !title ) {
64 | return;
65 | }
66 |
67 | return `https://${wiki.domain}/wiki/${title.replace( / /g, '_' )}?oldid=${id}`;
68 | }
69 | );
70 | };
71 |
72 | export const getTimelineRevisions = createSelector(
73 | state => state.revisions.list,
74 | ( revisions ) => (
75 | revisions
76 | .filter( revision => revision.meta.interaction )
77 | .groupBy( revision => revision.timestamp.clone().startOf( 'day' ) )
78 | )
79 | );
80 |
--------------------------------------------------------------------------------
/i18n/li.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Ooswesthoesbes"
5 | ]
6 | },
7 | "app-description": "Chronologische gesjiechte van twieë gebroekers op pagina's worop zie allebei bewirkinge höbbe gemaak.",
8 | "app-feedback-link": "Laes mier & gaef feedback!",
9 | "app-title": "Interaksjetiedlien",
10 | "back-top": "Trök nao baove",
11 | "between-edits": "$1 tösse bewirkinge",
12 | "between-interactions": "$1 tösje interaksjes",
13 | "error": "Fout",
14 | "error-help": "$1, en daonao $2.",
15 | "error-help-report": "Rapporteer de fout",
16 | "error-help-refresh": "vernuuj de pagina",
17 | "error-message-request-url": "Vraog URL op",
18 | "error-suppressed-diff": "Doe kans dit versjil neet zeen, went ein of allebei de bewirkinge zint óngerdrök.",
19 | "info-no-users": "Gaef estebleef twie gebroekers op veur te beginne.",
20 | "info-no-wiki": "Gaef estebleef 'ne wiki op veur te beginne.",
21 | "info-required-fields": "Gaef estebleef twie gebroekers en 'ne wiki op veur te beginne.",
22 | "licensed-under": "Oetgegaove ónger $1",
23 | "field-label-end-date": "Verloupdatum",
24 | "field-label-start-date": "Begindatum",
25 | "field-label-users": "Gebroekers",
26 | "field-label-wiki": "Wiki",
27 | "field-select-placeholder": "Sillekteer...",
28 | "field-select-no-results": "Gein rizzeltaote gevónje",
29 | "field-select-search-prompt": "Tik veur te zeuke",
30 | "made-by": "Gemaak door $1 van de Wikimedia Foundation.",
31 | "made-by-team": "Anti-Harassment Tools team",
32 | "privacy-policy": "Privaatbeleid",
33 | "powered-by": "Óngerstäönd door Wikimedia Toolforge",
34 | "report-bug": "Melj perbleem",
35 | "revision-edit-summary-removed": "(bewirkingssamevatting eweggesjaf)",
36 | "view-source-code-on": "Betrach op $1",
37 | "warning-no-results": "Gein rizzeltaote"
38 | }
39 |
--------------------------------------------------------------------------------
/client/src/components/timeline/date-revisions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Subject } from 'rxjs';
4 | import createIntersectionObservable from 'app/utils/intersection';
5 | import DateListContainer from './date-list.container';
6 |
7 | class DateRevisions extends React.Component {
8 | constructor( props ) {
9 | super( props );
10 |
11 | this.bottom = React.createRef();
12 | this.invokeFetch = new Subject();
13 | }
14 |
15 | componentDidMount() {
16 |
17 | const options = {
18 | rootMargin: '0px 0px 20% 0px'
19 | };
20 |
21 | // Create the infinite scroll.
22 | this.infinite = createIntersectionObservable( this.bottom.current, options )
23 | .map( entry => entry.isIntersecting )
24 | .filter( isBottomVisable => isBottomVisable )
25 | .filter( () => this.props.status === 'ready' )
26 | .filter( () => !this.props.empty )
27 | .debounceTime( 250 )
28 | .subscribe( () => {
29 | // The debounce delays the immisions so the props may not be the same.
30 | if ( this.props.status !== 'ready' ) {
31 | return;
32 | }
33 |
34 | if ( this.props.empty ) {
35 | return;
36 | }
37 |
38 | return this.props.fetchList();
39 | } );
40 | }
41 |
42 | componentWillUnmount() {
43 | this.infinite.unsubscribe();
44 | }
45 |
46 | render() {
47 | return (
48 |
54 | );
55 | }
56 | }
57 |
58 | DateRevisions.propTypes = {
59 | empty: PropTypes.bool.isRequired,
60 | status: PropTypes.oneOf( [ 'notready', 'ready', 'fetching', 'done', 'error' ] ).isRequired,
61 | fetchList: PropTypes.func.isRequired
62 | };
63 |
64 | export default DateRevisions;
65 |
--------------------------------------------------------------------------------
/client/src/components/fields/date-picker.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import moment from 'moment';
4 | import Datetime from 'react-datetime';
5 | import 'react-datetime/css/react-datetime.css';
6 |
7 | class DatePicker extends React.Component {
8 |
9 | constructor( props ) {
10 | super( props );
11 |
12 | // Binding
13 | this.onChange = this.onChange.bind( this );
14 | }
15 |
16 | onChange( date ) {
17 | // Ensure that the date is a valid date object or an empty string.
18 | if ( date instanceof moment || !date ) {
19 | // Ensure that the date has actually changed.
20 | if ( this.props.value !== date ) {
21 | // If the date is a moment object and the component was provided a
22 | // function to validate the date, ensure the date is valid before
23 | // passing it upstream.
24 | if ( date instanceof moment && this.props.isValidDate ) {
25 | if ( this.props.isValidDate( date ) ) {
26 | return this.props.onChange( date );
27 | }
28 | } else {
29 | this.props.onChange( date );
30 | }
31 | }
32 | }
33 | }
34 |
35 | render() {
36 | return (
37 |
50 | );
51 | }
52 | }
53 |
54 | DatePicker.propTypes = {
55 | value: PropTypes.instanceOf( moment ),
56 | id: PropTypes.string,
57 | onChange: PropTypes.func.isRequired,
58 | isValidDate: PropTypes.func
59 | };
60 |
61 | DatePicker.defaultProps = {
62 | value: undefined,
63 | id: undefined,
64 | isValidDate: undefined
65 | };
66 |
67 | export default DatePicker;
68 |
--------------------------------------------------------------------------------
/client/src/components/error-boundary.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Message } from '@wikimedia/react.i18n';
4 | import 'material-design-icons/iconfont/material-icons.css';
5 |
6 | class ErrorBoundary extends React.PureComponent {
7 |
8 | constructor( props ) {
9 | super( props );
10 | this.state = {
11 | error: null
12 | };
13 |
14 | this.handleRefresh = this.handleRefresh.bind( this );
15 | }
16 |
17 | componentDidCatch( error ) {
18 | this.setState( {
19 | error
20 | } );
21 | }
22 |
23 | handleRefresh() {
24 | const doc = this.refreshLink.ownerDocument;
25 | const win = doc.defaultView || doc.parentWindow;
26 |
27 | return win.location.reload( true );
28 | }
29 |
30 | render() {
31 | if ( this.state.error === null ) {
32 | return this.props.children;
33 | }
34 |
35 | const report = (
36 |
37 |
38 |
39 | );
40 |
41 | const refresh = (
42 | { this.refreshLink = refreshLink; }}
45 | onKeyPress={this.handleRefresh}
46 | onClick={this.handleRefresh}
47 | >
48 |
49 |
50 | );
51 |
52 | return (
53 |
54 |
55 |
error_outline
56 |
57 |
{this.state.error.message}
58 |
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | ErrorBoundary.propTypes = {
66 | children: PropTypes.node.isRequired
67 | };
68 |
69 | export default ErrorBoundary;
70 |
--------------------------------------------------------------------------------
/i18n/zh-hant.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Cookai1205",
5 | "Diskdance",
6 | "Ericliu1912",
7 | "Kly",
8 | "Sanmosa",
9 | "Tranve",
10 | "Winston Sung",
11 | "捍粵者"
12 | ]
13 | },
14 | "app-description": "顯示兩名使用者在曾編輯過的同一個頁面裡順序歷史記錄。",
15 | "app-feedback-link": "說明",
16 | "app-title": "互動時間軸",
17 | "back-top": "返回頂端",
18 | "between-edits": "在 $1期間的編輯",
19 | "between-interactions": "在 $1期間的互動",
20 | "error": "錯誤",
21 | "error-help": "$1,然後是$2。",
22 | "error-help-report": "回報錯誤",
23 | "error-help-refresh": "重新整理頁面",
24 | "error-message-request-url": "請求 URL",
25 | "error-suppressed-diff": "因修訂版本已被隱藏,您無法檢視差異。",
26 | "info-no-users": "請提供兩名使用者以開始",
27 | "info-no-wiki": "請提供wiki來開始",
28 | "info-required-fields": "請提供兩名使用者與wiki來開始。",
29 | "licensed-under": "使用 $1 條款授權",
30 | "field-label-end-date": "結束日期",
31 | "field-label-start-date": "開始日期",
32 | "field-label-users": "使用者",
33 | "field-label-wiki": "wiki",
34 | "field-select-placeholder": "選擇...",
35 | "field-select-no-results": "查無結果",
36 | "field-select-search-prompt": "要搜尋的類型",
37 | "made-by": "由維基媒體基金會的$1製作。",
38 | "made-by-team": "反騷擾工具團隊",
39 | "privacy-policy": "隱私權政策",
40 | "powered-by": "由維基媒體 Toolforge 提供支援",
41 | "report-bug": "回報程式問題",
42 | "revision-edit-summary-removed": "編輯摘要已移除",
43 | "view-source-code-on": "在 $1 上檢視",
44 | "warning-no-results": "沒有結果",
45 | "discuss-on-wiki": "分享這些結果",
46 | "discuss-on-wiki-text": "$1至$2期間在 $3 上,[[User:$4]]([[User talk:$4|討論]] | [[Special:Contributions/$4|貢獻]])與[[User:$5]]([[User talk:$5|討論]] | [[Special:Contributions/$5|貢獻]])有在同一頁面上做出編輯。您可以在 [$6 Interaction Timeline] 檢視他們所有編輯內容的事件順序清單,或是在 [$7 Interaction Analyser] 檢視表格視圖。",
47 | "discuss-on-wiki-help": "將此份 wikitext 複製並貼上到 wiki 上的討論區,來與其他人分享這些 Interaction Timeline 結果",
48 | "discuss-on-wiki-copy": "複製",
49 | "discuss-on-wiki-copied": "已複製!"
50 | }
51 |
--------------------------------------------------------------------------------
/i18n/ko.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Revi",
5 | "Ykhwong",
6 | "그냥기여자",
7 | "아라"
8 | ]
9 | },
10 | "app-description": "두 사용자가 문서에 편집한 시간 순서에 따른 역사입니다.",
11 | "app-feedback-link": "도움말",
12 | "app-title": "상호작용 타임라인",
13 | "back-top": "맨 위로 돌아가기",
14 | "between-edits": "편집 간 $1",
15 | "between-interactions": "상호 작용 사이에 $1",
16 | "error": "오류",
17 | "error-help": "$1, 그리고 $2.",
18 | "error-help-report": "오류 보고",
19 | "error-help-refresh": "문서 새로 고침",
20 | "error-message-request-url": "요청 URL",
21 | "error-suppressed-diff": "판 하나 또는 두 판 모두 숨겨져 있으므로 이 차이를 볼 수 없습니다.",
22 | "info-no-users": "시작할 두 사용자를 지정해 주십시오.",
23 | "info-no-wiki": "시작할 위키를 지정해 주십시오.",
24 | "info-required-fields": "시작할 두 사용자와 위키를 지정해 주십시오.",
25 | "licensed-under": "$1로 라이선스됨",
26 | "field-label-end-date": "종료일",
27 | "field-label-start-date": "시작일",
28 | "field-label-users": "사용자",
29 | "field-label-wiki": "위키",
30 | "field-select-placeholder": "선택...",
31 | "field-select-no-results": "결과가 없습니다",
32 | "field-select-search-prompt": "검색하려면 입력하세요",
33 | "made-by": "위키미디어 재단의 $1님이 만들었습니다.",
34 | "made-by-team": "괴롭힘 대응 도구 팀",
35 | "privacy-policy": "개인정보보호정책",
36 | "powered-by": "Wikimedia Toolforge에 의해 지원됨",
37 | "report-bug": "버그 보고",
38 | "revision-edit-summary-removed": "편집 요약 삭제됨",
39 | "view-source-code-on": "$1에서 보기",
40 | "warning-no-results": "결과 없음",
41 | "discuss-on-wiki": "이 결과 공유",
42 | "discuss-on-wiki-text": "$1에서 $2 사이 $3 [[User:$4]] ([[User talk:$4|토론]] | [[Special:Contributions/$4|기여]])님과 [[User:$5]] ([[User talk:$5|토론]] | [[Special:Contributions/$5|기여]])님이 일부 페이지에 대하여 공동으로 편집했습니다. [$6 상호작용 타임라인]에서 시간 순서대로 나열한 목록으로 편집 사항을 보거나 [$7 상호작용 분석]에서 표의 형태로 편집 사항을 볼 수 있습니다.",
43 | "discuss-on-wiki-help": "이 상호작용 타임라인 결과를 다른 사람과 공유하려면 위키 토론에 이 위키텍스트를 복사해서 붙여넣으십시오",
44 | "discuss-on-wiki-copy": "복사",
45 | "discuss-on-wiki-copied": "복사 완료!"
46 | }
47 |
--------------------------------------------------------------------------------
/i18n/cs.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Dvorapa",
5 | "Ilimanaq29",
6 | "Korytaacheck",
7 | "Patriccck",
8 | "Patrik L."
9 | ]
10 | },
11 | "app-description": "Chronologická historie dvou uživatelů na stránkách, na kterých oba editovali.",
12 | "app-feedback-link": "Nápověda",
13 | "app-title": "Interaction Timeline",
14 | "back-top": "Zpět na začátek",
15 | "between-edits": "$1 mezi editacemi",
16 | "between-interactions": "$1 mezi interakcemi",
17 | "error": "Chyba",
18 | "error-help": "$1, a potom $2.",
19 | "error-help-report": "Nahlásit chybu",
20 | "error-help-refresh": "Načíst stránku znovu",
21 | "error-message-request-url": "Požádat o URL",
22 | "error-suppressed-diff": "Nemůžete si prohlédnout tento rozdíl verzí, protože jedna nebo obě z těchto revizí byly smazány.",
23 | "info-no-users": "Pro začátek prosím vložte dva uživatele.",
24 | "info-no-wiki": "Pro začátek prosím vložte wiki.",
25 | "info-required-fields": "Pro začátek prosím vložte dva uživatele.",
26 | "licensed-under": "Licencováno pod $1",
27 | "field-label-end-date": "Datum ukončení",
28 | "field-label-start-date": "Datum zahájení",
29 | "field-label-users": "Uživatelé",
30 | "field-label-wiki": "Wiki",
31 | "field-select-placeholder": "Zvolit...",
32 | "field-select-no-results": "Nenalezeny žádné výsledky",
33 | "field-select-search-prompt": "Napište k vyhledání",
34 | "made-by": "Vytvořeno Nadací Wikimedia $1.",
35 | "made-by-team": "Tým nástrojů Anti-Harassment",
36 | "privacy-policy": "Zásady ochrany osobních údajů",
37 | "powered-by": "Napájeno Wikimedia Toolforge",
38 | "report-bug": "Nahlásit chybu",
39 | "revision-edit-summary-removed": "Shrnutí editace odstraněno",
40 | "view-source-code-on": "Zobrazit na $1",
41 | "warning-no-results": "Žádné výsledky",
42 | "discuss-on-wiki": "Sdílet tyto výsledky",
43 | "discuss-on-wiki-copy": "Kopírovat"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/ja.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Gulpin",
5 | "Omotecho",
6 | "Shirayuki",
7 | "Suyama"
8 | ]
9 | },
10 | "app-description": "2人の利用者がともに編集した複数ページの時刻順の履歴。",
11 | "app-feedback-link": "ヘルプ",
12 | "app-title": "インタラクションのタイムライン",
13 | "back-top": "トップに戻る",
14 | "between-edits": "$1 前に編集",
15 | "between-interactions": "$1前にインタラクション",
16 | "error": "エラー",
17 | "error-help": "$1して$2してください",
18 | "error-help-report": "エラーを報告",
19 | "error-help-refresh": "ページを更新",
20 | "error-message-request-url": "リクエスト URL",
21 | "error-suppressed-diff": "少なくともどちらかの版が秘匿されているため、この差分を閲覧できません。",
22 | "info-no-users": "比較する利用者2人を指定してください。",
23 | "info-no-wiki": "調べるウィキを指定してください。",
24 | "info-required-fields": "比較する利用者2人とウィキを指定してください。",
25 | "licensed-under": "$1 でライセンスされています。",
26 | "field-label-end-date": "終了日",
27 | "field-label-start-date": "開始日",
28 | "field-label-users": "利用者",
29 | "field-label-wiki": "ウィキ",
30 | "field-select-placeholder": "選択...",
31 | "field-select-no-results": "見つかりませんでした",
32 | "field-select-search-prompt": "入力して検索",
33 | "made-by": "制作はウィキメディア財団の$1が担当しました。",
34 | "made-by-team": "嫌がらせ対策ツール・チーム",
35 | "privacy-policy": "プライバシー・ポリシー",
36 | "powered-by": "ウィキメディア Toolforge 提供",
37 | "report-bug": "バグを報告",
38 | "revision-edit-summary-removed": "編集要約を削除しました",
39 | "view-source-code-on": "$1で閲覧",
40 | "warning-no-results": "該当結果はありません。",
41 | "discuss-on-wiki": "これらの結果を共有",
42 | "discuss-on-wiki-text": "$1 から $2 の間に $3 で[[User:$4]] ([[User talk:$4|talk]] | [[Special:Contributions/$4|contribs]]) と [[User:$5]] ([[User talk:$5|talk]] | [[Special:Contributions/$5|contribs]]) の両者とも編集したページが複数あります。それらの編集について時刻順の一覧を見るには[$6 Interaction Timeline]または[$7 Interaction Analyser]から今日形式で表示できます。",
43 | "discuss-on-wiki-help": "このウィキ文をオンウィキの議論のページにコピー&ペーストすると他の人と Interaction Timeline の結果を共有できます",
44 | "discuss-on-wiki-copy": "コピー",
45 | "discuss-on-wiki-copied": "コピーしました"
46 | }
47 |
--------------------------------------------------------------------------------
/i18n/th.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "B20180",
5 | "Prame Tan"
6 | ]
7 | },
8 | "app-description": "ประวัติตามลำดับเวลาสำหรับผู้ใช้สองคนในหน้าเว็บที่พวกเขาได้ทำการแก้ไขทั้งคู่",
9 | "app-feedback-link": "ช่วยเหลือ",
10 | "app-title": "เส้นเวลาการโต้ตอบ",
11 | "back-top": "กลับไปด้านบน",
12 | "between-edits": "$1 ระหว่างการแก้ไข",
13 | "between-interactions": "$1 ระหว่างการโต้ตอบ",
14 | "error": "ข้อผิดพลาด",
15 | "error-help": "$1 แล้ว $2",
16 | "error-help-report": "รายงานปัญหา",
17 | "error-help-refresh": "รีเฟรชหน้าใหม่",
18 | "error-message-request-url": "ขอ URL",
19 | "info-no-users": "กรุณาระบุผู้ใช้สองคนเพื่อเริ่มต้น",
20 | "info-no-wiki": "กรุณาระบุวิกิเพื่อเริ่มต้น",
21 | "info-required-fields": "กรุณาระบุผู้ใช้สองคนและวิกิเพื่อเริ่มต้น",
22 | "licensed-under": "อยู่ภายใต้สัญญาอนุญาต $1",
23 | "field-label-end-date": "วันที่สิ้นสุด",
24 | "field-label-start-date": "วันที่เริ่มต้น",
25 | "field-label-users": "ผู้ใช้",
26 | "field-label-wiki": "วิกิ",
27 | "field-select-placeholder": "เลือก...",
28 | "field-select-no-results": "ไม่พบผลลัพธ์",
29 | "field-select-search-prompt": "พิมพ์เพื่อค้นหา",
30 | "made-by-team": "ทีมงานเครื่องมือป้องกันการคุกคาม",
31 | "privacy-policy": "นโยบายความเป็นส่วนตัว",
32 | "report-bug": "รายงานข้อผิดพลาด",
33 | "revision-edit-summary-removed": "(ความย่อการแก้ไขถูกลบ)",
34 | "view-source-code-on": "ดูบน $1",
35 | "warning-no-results": "ไม่มีผลลัพธ์",
36 | "discuss-on-wiki": "แชร์ผลลัพธ์เหล่านี้",
37 | "discuss-on-wiki-text": "ระหว่างวันที่ $1 ถึง $2 บน $3 ผู้ใช้[[User:$4]] ([[User talk:$4|talk]] | [[Special:Contributions/$4|contribs]])และ[[User:$5]] ([[User talk:$5|talk]] | [[Special:Contributions/$5|contribs]])ได้แก้ไขบนหน้าบางหน้าร่วมกัน คุณสามารถดูรายชื่อการแก้ไขทั้งหมดเรียงตามเวลาได้ที่[$6 Interaction Timeline] หรือผ่านแบบตารางบน [$7 Interaction Analyser]",
38 | "discuss-on-wiki-copy": "คัดลอก",
39 | "discuss-on-wiki-copied": "คัดลอกเสร็จสมบูรณ์"
40 | }
41 |
--------------------------------------------------------------------------------
/server/src/Service/ConnectionService.php:
--------------------------------------------------------------------------------
1 | params = $params;
32 | $this->config = $config;
33 | }
34 |
35 | /**
36 | * @return Connection
37 | * @throws \Exception
38 | */
39 | public function getConnection() {
40 | if ( !$this->conn ) {
41 | throw new \Exception( 'no connection' );
42 | }
43 |
44 | return $this->conn;
45 | }
46 |
47 | /**
48 | * @param string $wiki
49 | * @return Connection
50 | */
51 | public function connect( $wiki ) {
52 | if ( !isset( $this->conn ) ) {
53 | $params = [
54 | 'dbname' => $this->getDBName( $wiki ),
55 | 'user' => $this->params['user'],
56 | 'password' => $this->params['pass'],
57 | 'host' => $this->getHost( $wiki ),
58 | 'port' => $this->params['port'],
59 | 'driver' => 'pdo_mysql'
60 | ];
61 |
62 | $this->conn = DriverManager::getConnection( $params, $this->config );
63 | }
64 |
65 | return $this->conn;
66 | }
67 |
68 | /**
69 | * @param string $wiki
70 | * @return string
71 | */
72 | private function getHost( $wiki ) {
73 | if ( $this->params['cluster'] ) {
74 | return $wiki . '.' . $this->params['cluster'];
75 | }
76 |
77 | return $this->params['host'];
78 | }
79 |
80 | /**
81 | * @param string $wiki
82 | * @return string
83 | */
84 | private function getDBName( $wiki ) {
85 | return $wiki . '_p';
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/client/src/components/timeline/diff/header-revision.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Message } from '@wikimedia/react.i18n';
4 | import RevisionEntity from 'app/entities/revision';
5 | import Header from 'app/components/timeline/header';
6 | import Spinner from 'app/components/timeline/spinner';
7 |
8 | class HeaderRevision extends React.PureComponent {
9 |
10 | componentDidMount() {
11 | if ( !this.props.revision ) {
12 | return;
13 | }
14 |
15 | if ( this.props.revision.meta.status === 'ready' ) {
16 | this.props.fetchRevision( this.props.revision.id );
17 | }
18 | }
19 |
20 | render() {
21 | if ( !this.props.revision ) {
22 | return (
23 |
24 | );
25 | }
26 |
27 | if ( this.props.revision.meta.status === 'ready' ) {
28 | return (
29 |
30 | );
31 | }
32 |
33 | if ( this.props.revision.meta.status === 'fetching' ) {
34 | return (
35 |
38 | );
39 | }
40 |
41 | if ( this.props.revision.meta.status === 'error' ) {
42 | return (
43 |
44 |
45 | {this.props.revision.meta.error.message}
46 |
47 | );
48 | }
49 |
50 | return (
51 |
52 | {this.props.revision.user}
53 | {this.props.revision.timestamp.format( 'YYYY-MM-DD' )} — {this.props.revision.timestamp.format( 'HH:mm' )}
54 |
55 | );
56 | }
57 | }
58 |
59 | HeaderRevision.propTypes = {
60 | fetchRevision: PropTypes.func.isRequired,
61 | revision: PropTypes.instanceOf( RevisionEntity ),
62 | url: PropTypes.string,
63 | side: PropTypes.string
64 | };
65 |
66 | HeaderRevision.defaultProps = {
67 | revision: undefined,
68 | url: undefined,
69 | side: undefined
70 | };
71 |
72 | export default HeaderRevision;
73 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { createStore, applyMiddleware } from 'redux';
5 | import { Provider } from 'react-redux';
6 | import createHistory from 'history/createBrowserHistory';
7 | import { ConnectedRouter, routerMiddleware } from 'react-router-redux';
8 | import { createEpicMiddleware } from 'redux-observable';
9 | import { composeWithDevTools } from 'redux-devtools-extension';
10 | import 'intersection-observer'; // Pollyfill
11 | import moment from 'moment';
12 | import { IntlProvider } from '@wikimedia/react.i18n';
13 | import i18n from './i18n.dir';
14 | import App from './src/components/app';
15 | import reducer from './src/reducers/index';
16 | import epic from './src/epics/index';
17 | import './styles/styles.scss';
18 |
19 | function main() {
20 | // Setup i18n data.
21 | const fileNames = Object.keys( i18n );
22 | const locale = navigator.language;
23 |
24 | // Create a messages object.
25 | const messages = fileNames.reduce( ( m, name ) => {
26 | m[ name.replace( /.json$/g, '' ) ] = {
27 | ...i18n[ name ].src
28 | };
29 |
30 | return m;
31 | }, {} );
32 |
33 | // Set the language globally for moment.
34 | moment.locale( locale );
35 |
36 | // Create a history of your choosing (we're using a browser history in this case)
37 | const history = createHistory();
38 | // Build the middleware for intercepting and dispatching navigation actions
39 | const router = routerMiddleware( history );
40 | const epicMiddleware = createEpicMiddleware( epic );
41 | // Add the reducer to your store on the `router` key
42 | // Also apply our middleware for navigating
43 | const store = createStore(
44 | reducer,
45 | composeWithDevTools( applyMiddleware( router, epicMiddleware ) ),
46 | );
47 |
48 | ReactDOM.render(
49 |
50 |
51 |
52 |
53 |
54 |
55 | ,
56 | document.getElementById( 'root' ),
57 | );
58 | }
59 |
60 | // Engage!
61 | main();
62 |
--------------------------------------------------------------------------------
/i18n/he.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Amire80",
5 | "Deborahjay",
6 | "Guycn2"
7 | ]
8 | },
9 | "app-description": "היסטוריה כרונולוגית של שני משתמשים בדפים שבהם שניהם עשו עריכות.",
10 | "app-feedback-link": "עזרה",
11 | "app-title": "ציר זמן של אינטראקציה",
12 | "back-top": "חזרה למעלה",
13 | "between-edits": "$1 בין עריכות",
14 | "between-interactions": "$1 בין אינטראקציות",
15 | "error": "שגיאה",
16 | "error-help": "$1, ואז $2.",
17 | "error-help-report": "דווחו את השגיאה",
18 | "error-help-refresh": "רעננו את הדף",
19 | "error-message-request-url": "ה־URL של הבקשה",
20 | "error-suppressed-diff": "לא ניתן להציג את ההשוואה הזאת כי אחת מהגרסאות הועלמה.",
21 | "info-no-users": "נא לתת שני משתמשים כדי להתחיל.",
22 | "info-no-wiki": "נא לתת ויקי כדי להתחיל.",
23 | "info-required-fields": "נא לתת שני משתמשים וויקי כדי להתחיל.",
24 | "licensed-under": "ברישיון $1",
25 | "field-label-end-date": "תאריך סיום",
26 | "field-label-start-date": "תאריך התחלה",
27 | "field-label-users": "משתמשים",
28 | "field-label-wiki": "ויקי",
29 | "field-select-placeholder": "לבחור...",
30 | "field-select-no-results": "לא נמצאו תוצאות",
31 | "field-select-search-prompt": "נא להקליד כדי לחפש",
32 | "made-by": "נוצר על־ידי $1 של קרן ויקימדיה",
33 | "made-by-team": "צוות הכלים נגד הטרדה",
34 | "privacy-policy": "מדיניות פרטיות",
35 | "powered-by": "מופעל על־ידי מעבדות כלי ויקימדיה",
36 | "report-bug": "דיווח באג",
37 | "revision-edit-summary-removed": "תקציר העריכה הוסר",
38 | "view-source-code-on": "להציג באתר $1",
39 | "warning-no-results": "אין תוצאות",
40 | "discuss-on-wiki": "שיתוף התוצאות האלו",
41 | "discuss-on-wiki-text": "מ־$1 עד $2 ב־$3 [[User:$4|משתמש:$]] ([[User talk:$4|שיחה]] | [[Special:Contributions/$4|תרומות]]) ו[[User:$5|משתמש:$5]] ([[User talk:$5|שיחה]] | [[Special:Contributions/$5|תרומות]]) עשו עריכות למספר דפים במשותף. באפשרותך לראות רשימה כרונולוגית של כל העריכות שלהם ב[$6 ציר זמן של אינטראקציה] או בתצוגת טבלה ב[$7 מנתח אינטראקציה].",
42 | "discuss-on-wiki-help": "העתיקו והדביקו את קוד הוויקי הזה לדיון בוויקי כדי לשתף את התוצאות האלה של ציר הזמן של אינטראקציה עם אחרים",
43 | "discuss-on-wiki-copy": "העתקה",
44 | "discuss-on-wiki-copied": "הועתק!"
45 | }
46 |
--------------------------------------------------------------------------------
/i18n/ar.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Meno25",
5 | "Sonic N800",
6 | "ديفيد",
7 | "محمد أحمد عبد الفتاح"
8 | ]
9 | },
10 | "app-description": "سجل زمني لمستخدمين في الصفحات التي أجروا فيها تعديلات.",
11 | "app-feedback-link": "مساعدة",
12 | "app-title": "الجدول الزمني للتفاعل",
13 | "back-top": "العودة إلى الأعلى",
14 | "between-edits": "$1 بين التعديلات",
15 | "between-interactions": "$1 بين التفاعلات",
16 | "error": "خطأ",
17 | "error-help": "$1، ثم $2.",
18 | "error-help-report": "أبلغ عن الخطأ",
19 | "error-help-refresh": "تحديث الصفحة",
20 | "error-message-request-url": "طلب مسار",
21 | "error-suppressed-diff": "لا يمكنك مشاهدة هذا الفرق بسبب إخفاء إحدى المراجعتين أو كليهما.",
22 | "info-no-users": "يُرجَى تقديم مستخدمين اثنين للبدء.",
23 | "info-no-wiki": "يُرجَى تقديم ويكي للبدء.",
24 | "info-required-fields": "يُرجَى تقديم مستخدمين اثنين وويكي للبدء.",
25 | "licensed-under": "مرخص تحت $1",
26 | "field-label-end-date": "تاريخ الانتهاء",
27 | "field-label-start-date": "تاريخ البدء",
28 | "field-label-users": "المستخدمون",
29 | "field-label-wiki": "الويكي",
30 | "field-select-placeholder": "تحديد...",
31 | "field-select-no-results": "لا توجد نتائج",
32 | "field-select-search-prompt": "اكتب للبحث",
33 | "made-by": "من صنع مؤسسة ويكيميديا $1.",
34 | "made-by-team": "فريق أدوات مكافحة التحرش",
35 | "privacy-policy": "سياسة الخصوصية",
36 | "powered-by": "مدعوم من ويكيميديا تولدورج",
37 | "report-bug": "الإبلاغ عن خطأ",
38 | "revision-edit-summary-removed": "أزيل ملخص التعديل",
39 | "view-source-code-on": "عرض في $1",
40 | "warning-no-results": "لا توجد نتائج",
41 | "discuss-on-wiki": "شارك هذه النتائج",
42 | "discuss-on-wiki-text": "ما بين $1 و $2 في $3 [[User:$4]] ([[User talk:$4|talk]] | [[Special:Contributions/$4|contribs]]) و [[User:$5]] ([[User talk:$5|talk]] | [[Special:Contributions/$5|contribs]]) كلاهما قاما بتعديلات على بعض الصفحات. يمكنك أن ترى قائمة مرتبة زمنيا لكل تعديلاتهم على [$6 Interaction Timeline] أو عرض في جدول على [$7 Interaction Analyser].",
43 | "discuss-on-wiki-help": "انسخ والصق نص الويكي هذا لنقاش على الويكي لمشاركة نتائج الخط الزمني للمشاركة مع الآخرين",
44 | "discuss-on-wiki-copy": "نسخ",
45 | "discuss-on-wiki-copied": "نسخت!"
46 | }
47 |
--------------------------------------------------------------------------------
/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "app-description": "Chronological history for two users on pages where they have both made edits.",
3 | "app-feedback-link": "Help",
4 | "app-title": "Interaction Timeline",
5 | "back-top": "Back to Top",
6 | "between-edits": "$1 between edits",
7 | "between-interactions": "$1 between interactions",
8 | "error": "Error",
9 | "error-help": "$1, and then $2.",
10 | "error-help-report": "Report the error",
11 | "error-help-refresh": "refresh the page",
12 | "error-message-request-url": "Request URL",
13 | "error-suppressed-diff": "You cannot view this diff because one or both of the revisions has been suppressed.",
14 | "info-no-users": "Please provide two users to begin.",
15 | "info-no-wiki": "Please provide a wiki to begin.",
16 | "info-required-fields": "Please provide two users and wiki to begin.",
17 | "licensed-under": "Licensed under $1",
18 | "field-label-end-date": "End Date",
19 | "field-label-start-date": "Start Date",
20 | "field-label-users": "Users",
21 | "field-label-wiki": "Wiki",
22 | "field-select-placeholder": "Select...",
23 | "field-select-no-results": "No results found",
24 | "field-select-search-prompt": "Type to search",
25 | "made-by": "Made by the Wikimedia Foundation's $1.",
26 | "made-by-team": "Anti-Harassment Tools team",
27 | "privacy-policy": "Privacy policy",
28 | "powered-by": "Powered by Wikimedia Toolforge",
29 | "report-bug": "Report a bug",
30 | "revision-edit-summary-removed": "edit summary removed",
31 | "view-source-code-on": "View on $1",
32 | "warning-no-results": "No Results",
33 | "discuss-on-wiki": "Share these results",
34 | "discuss-on-wiki-text": "Between $1 and $2 on $3 [[User:$4]] ([[User talk:$4|talk]] | [[Special:Contributions/$4|contribs]]) and [[User:$5]] ([[User talk:$5|talk]] | [[Special:Contributions/$5|contribs]]) both made edits to some pages in common. You can see a chronological list of all their edits on the [$6 Interaction Timeline] or a table view on the [$7 Interaction Analyser].",
35 | "discuss-on-wiki-help": "Copy & paste this wikitext to an on-wiki discussion to share these Interaction Timeline results with others",
36 | "discuss-on-wiki-copy": "Copy",
37 | "discuss-on-wiki-copied": "Copied!"
38 | }
39 |
--------------------------------------------------------------------------------
/server/src/dependencies.php:
--------------------------------------------------------------------------------
1 | getContainer();
4 |
5 | // monolog
6 | $container['logger'] = function ( $c ) {
7 | $settings = $c->get( 'settings' );
8 | $logger = new Monolog\Logger( $settings['logger']['name'] );
9 | $handler = new Monolog\Handler\StreamHandler( $settings['logger']['path'] );
10 | $logger->pushHandler( $handler, $settings['logger']['level'] );
11 |
12 | return $logger;
13 | };
14 |
15 | // error handler
16 | $container['errorHandler'] = function ( $c ) {
17 | $logger = $c->get( 'logger' );
18 | return new App\AppErrorHandler( $logger );
19 | };
20 |
21 | $container['redis'] = function ( $c ) {
22 | $config = $c->get( 'settings' )['redis'];
23 |
24 | $redis = new \Redis();
25 | $redis->connect( $config['host'], $config['port'] );
26 | $redis->setOption( \Redis::OPT_PREFIX, $config['prefix'] );
27 |
28 | return $redis;
29 | };
30 |
31 | $container['dbalCache'] = function ( $c ) {
32 | $cacheProvider = new Doctrine\Common\Cache\RedisCache();
33 | $cacheProvider->setRedis( $c->get( 'redis' ) );
34 |
35 | return $cacheProvider;
36 | };
37 |
38 | $container['connectionService'] = function ( $c ) {
39 | $params = $c->get( 'settings' )['db'];
40 | $cache = $c->get( 'dbalCache' );
41 |
42 | $config = new \Doctrine\DBAL\Configuration();
43 | $config->setResultCacheImpl( $cache );
44 |
45 | $connManager = new App\Service\ConnectionService( $params, $config );
46 |
47 | return $connManager;
48 | };
49 |
50 | $container['revisionDao'] = function ( $c ) {
51 | return new App\Dao\RevisionDao(
52 | $c->get( 'connectionService' ),
53 | $c->get( 'logger' )
54 | );
55 | };
56 |
57 | $container['userDao'] = function ( $c ) {
58 | return new App\Dao\UserDao(
59 | $c->get( 'connectionService' ),
60 | $c->get( 'logger' )
61 | );
62 | };
63 |
64 | $container['interactionService'] = function ( $c ) {
65 | $revisionDao = $c->get( 'revisionDao' );
66 | $userDao = $c->get( 'userDao' );
67 | $service = new App\Service\InteractionService( $revisionDao, $userDao );
68 | return $service;
69 | };
70 |
71 | // routes
72 | $container[App\Action\InteractionAction::class] = function ( $c ) {
73 | $service = $c->get( 'interactionService' );
74 | return new App\Action\InteractionAction( $service );
75 | };
76 |
--------------------------------------------------------------------------------
/i18n/ps.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Ahmed-Najib-Biabani-Ibrahimkhel",
5 | "شاه زمان پټان"
6 | ]
7 | },
8 | "app-description": "د دوو کارنانو لپاره د هغو مخونو چې دوی دواړو سمکړي دي د نېټو په ترتيب پېښليک",
9 | "app-feedback-link": "لارښود",
10 | "app-title": "د تعامل مهالوېش",
11 | "back-top": "بېرته سر ته",
12 | "between-edits": "$1 سمونونو ترمنځ",
13 | "between-interactions": "$1 تعاملاتو ترمنځ",
14 | "error": "تېروتنه",
15 | "error-help": "$1 او بيا $2.",
16 | "error-help-report": "د تېروتنې خبر ورکول",
17 | "error-help-refresh": "مخ تاندول",
18 | "error-message-request-url": "وېبتړ غوښتل",
19 | "error-suppressed-diff": "تاسو دا توپير نشئ کتلی، ځکه چې يو يا دواړه بياکتنې له منځه وړل شوې دي.",
20 | "info-no-users": "مهرباني وکړئ پيلولو لپاره دوه کارنان چمتو کړئ.",
21 | "info-no-wiki": "مهرباني وکړئ پيلولو لپاره يو ويکي چمتو کړئ.",
22 | "info-required-fields": "مهرباني وکړئ پيلولو لپاره دوه کارنان او ويکي چمتو کړئ.",
23 | "licensed-under": "$1 منښتليک سره سم",
24 | "field-label-end-date": "پای نېټه",
25 | "field-label-start-date": "پيل نېټه",
26 | "field-label-users": "کارنان",
27 | "field-label-wiki": "ويکي",
28 | "field-select-placeholder": "وټاکئ...",
29 | "field-select-no-results": "هېڅ پايلې ونه موندل شوې",
30 | "field-select-search-prompt": "پلټلو لپاره وټاپئ",
31 | "made-by": "ويکيرسنۍ بنسټ له خوا جوړ شوی $1.",
32 | "made-by-team": "ځورونې ضد توکو ډله",
33 | "privacy-policy": "پټنتيا تگلار",
34 | "powered-by": "د ويکيرسنۍ توکسپنوونکي له خوا پرمخوړل شوی",
35 | "report-bug": "تېروتنې خبر ورکول",
36 | "revision-edit-summary-removed": "سمون لنډيز لرېشو",
37 | "view-source-code-on": "په $1 باندې کتل",
38 | "warning-no-results": "بې پايلو",
39 | "discuss-on-wiki": "دا پايلې وېشل",
40 | "discuss-on-wiki-text": "د $1 او $2 ترمنځ په $3 کې [[کارن:$4]] ([[د کارن خبرې اترې:$4|خبرېاترې]] | [[ځانگړی:ونډې/$4|ونډې]]) او [[کارن:$5]] ([[د کارن خبرې اترې:$5|خبرې اترې]] | [[ځانگړی:ونډې/$5|ونډې]]) دواړو د خونديځ په ځينو مخونو کې سمونونه کړي. تاسو د دوی د سمونونو کرونولوژيکي لړليک په [$6 د تعامل مهالوېش] يا د [$7 تعامل شنونکي] لښتيال کې کتلی شئ.",
41 | "discuss-on-wiki-help": "دا ويکيليک ولمېسئ او په شننه کې يې ولېښئ؛ ترڅو د دې د تعامل مهالوېش پايلې له نورو سره شريکې شي.",
42 | "discuss-on-wiki-copy": "لمېسل",
43 | "discuss-on-wiki-copied": "ولمېسلشو!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/xmf.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Narazeni"
5 | ]
6 | },
7 | "app-description": "ჟირი მახვარებუშ ქრონოლოგიური ისტორია თი ხასჷლას, სოდე თინეფქ მიშეღეს რედაქტირაფან.",
8 | "app-feedback-link": "მოხვარა",
9 | "back-top": "ჟილე დორთა",
10 | "between-edits": "$1 რედაქტირაფეფს შქას",
11 | "between-interactions": "$1 ურთიართქიმინჯალაშ შქას",
12 | "error": "ჩილათა",
13 | "error-help": "$1, დო უკული $2.",
14 | "error-help-report": "ქუგმოგებაბეთ ჩილათაშ გეშა",
15 | "error-help-refresh": "ხასჷლაშ გოახალაფა",
16 | "error-message-request-url": "მოთხირიშ URL",
17 | "error-suppressed-diff": "თქვა ვეშეილებჷნა თე გინორთიშ ძირაფა, ანდანე ართი ვარ-და ჟირხოლო ვერსია ინოკიჩილი რე.",
18 | "info-no-users": "ქორთხინთ, ქემიოწურეთ ჟირი მახვარებუ.",
19 | "info-no-wiki": "ქორთხინთ, ქემიოწურეთ ვიკიპროექტი ანალიზიშო.",
20 | "info-required-fields": "ქორთხინთ, ქემიოწურეთ ჟირი მახვარებუ დო ვიკიპროექტი.",
21 | "licensed-under": "ლიცენზიათ $1",
22 | "field-label-end-date": "თებაშ თარიღი",
23 | "field-label-start-date": "დოჭყაფაშ თარიღი",
24 | "field-label-users": "მახვარებუეფი",
25 | "field-label-wiki": "ვიკი",
26 | "field-select-placeholder": "გეგშაგორი...",
27 | "field-select-no-results": "შედეგიქ ვეგორინჷ",
28 | "field-select-search-prompt": "ოგორალო გენშაჸონით",
29 | "made-by": "ნაქიმინა რე ფონდი ვიკიმედიაშ ბუნაშით $1.",
30 | "privacy-policy": "კონფიდენციალურალაშ პოლიტიკა",
31 | "powered-by": "პლატფორმას Wikimedia Toolforge",
32 | "report-bug": "ქუგმოგებაფეთ ჩილათაშ გეშა",
33 | "revision-edit-summary-removed": "რედაქტირაფაშ რეზიუმე ლასირი რე",
34 | "view-source-code-on": "ქოძირით $1-ის",
35 | "warning-no-results": "მუთუნქ ვეგორუ",
36 | "discuss-on-wiki": "ათე მოღალობეფიშ შელუა",
37 | "discuss-on-wiki-text": "$1-შ დო $2-შ შქას ვიკიპროექტის $3 ჟირი მახვარებუქ [[User:$4]] ([[User talk:$4|სხუ.]] | [[Special:Contributions/$4|თია]]) დო [[User:$5]] ([[User talk:$5|სხუ.]] | [[Special:Contributions/$5|თია]]) მიშეღეს რედაქტირაფა კანკალე ხასჷლეფშა. თქვა შეილებჷნა თინეფიშ არძა რედაქტირაფაშ ქრონოლოგიური ერკებულიშ ძირაფა [$6 Interaction Timeline]-ს ვარ-და ცხირიშ სახეთ [$7 Interaction Analyser]-ს.",
38 | "discuss-on-wiki-help": "დაკოპირით დო ქენახუნეთ ათე ტექსტი, Interaction Timeline-შ შხვა მოღალობეფიშ გაშელებერო",
39 | "discuss-on-wiki-copy": "კოპირაფა",
40 | "discuss-on-wiki-copied": "კოპირაფილი რე!"
41 | }
42 |
--------------------------------------------------------------------------------
/i18n/lt.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Eitvys200",
5 | "Manvydasz",
6 | "Nokeoo"
7 | ]
8 | },
9 | "app-description": "Chronologinė istorija dviems vartotojams puslapiuose, kuriuose jie abu atliko keitimus.",
10 | "app-feedback-link": "Pagalba",
11 | "app-title": "Interaction Timeline",
12 | "back-top": "Į viršų",
13 | "between-edits": "$1 tarp pakeitimų",
14 | "between-interactions": "$1 tarp sąveikų",
15 | "error": "Klaida",
16 | "error-help": "$1, tada $2.",
17 | "error-help-report": "Praneškite apie klaidą",
18 | "error-help-refresh": "atnaujinti puslapį",
19 | "error-message-request-url": "Užklausos URL",
20 | "info-no-users": "Norėdami pradėti, pateikite du naudotojus.",
21 | "info-no-wiki": "Norėdami pradėti, pateikite vikį.",
22 | "info-required-fields": "Norėdami pradėti, pateikite du naudotojus ir vikį.",
23 | "licensed-under": "Pagal $1 licenciją",
24 | "field-label-end-date": "Pabaigos data",
25 | "field-label-start-date": "Pradžios data",
26 | "field-label-users": "Naudotojai",
27 | "field-label-wiki": "Projektas",
28 | "field-select-placeholder": "Pasirinkti...",
29 | "field-select-no-results": "Rezultatų nerasta",
30 | "field-select-search-prompt": "Rašykite norėdami ieškoti",
31 | "made-by": "Sukūrė Vikimedijos fondo $1.",
32 | "made-by-team": "Kovos su priekabiavimu įrankių komanda",
33 | "privacy-policy": "Privatumo politika",
34 | "powered-by": "Įgalina „Wikimedia Toolforge“",
35 | "report-bug": "Pranešti apie klaidą",
36 | "revision-edit-summary-removed": "keitimo santrauka pašalinta",
37 | "view-source-code-on": "Rodyti $1",
38 | "warning-no-results": "Nėra rezultatų",
39 | "discuss-on-wiki": "Dalintis šiais rezultatais",
40 | "discuss-on-wiki-text": "Tarp $1 ir $2 $3 [[User:$4]] ([[User talk:$4|aptarimas]] | [[Special:Contributions/$4|indelis]]) ir [[User:$5]] ([[User talk:$5|aptarimas]] | [[Special:Contributions/$5|indelis]]) abu atliko pakeitimus sutampančiuose puslapiuose. Visų jų keitimų chronologinį sąrašą galite peržiūrėti [$6 Sąveikos laiko juostoje] arba lentelės forma [$7 Sąveikos analizatoriuje].",
41 | "discuss-on-wiki-help": "Nukopijuokite ir įklijuokite šį vikitekstą į viki diskusiją, kad bendrintumėte šiuos sąveikos laiko juostos rezultatus su kitais",
42 | "discuss-on-wiki-copy": "Kopijuoti",
43 | "discuss-on-wiki-copied": "Nukopijuota!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/eo.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Mirin"
5 | ]
6 | },
7 | "app-description": "Kronologia historio pri du uzantoj en paĝoj kiujn ambaŭ redaktis",
8 | "app-feedback-link": "Helpo",
9 | "app-title": "Interaga Kronologio",
10 | "back-top": "Reiri Supren",
11 | "between-edits": "$1 inter redaktoj",
12 | "between-interactions": "$1 inter interagoj",
13 | "error": "Eraro",
14 | "error-help": "$1, kaj poste $2.",
15 | "error-help-report": "Raporti la eraron",
16 | "error-help-refresh": "refreŝigi la paĝon",
17 | "error-message-request-url": "Peta retadreso",
18 | "error-suppressed-diff": "Vi ne povas vidi ĉi tiun diferenco, ĉar unu aŭ ambaŭ el la revizioj estis subpremita(j).",
19 | "info-no-users": "Bonvolu provizi du uzantojn por komenci.",
20 | "info-no-wiki": "Bonvolu provizi vikion por komenci.",
21 | "info-required-fields": "Bonvolu provizi du uzantojn kaj vikion por komenci.",
22 | "licensed-under": "Laŭ permesilo $1",
23 | "field-label-end-date": "Fina Dato",
24 | "field-label-start-date": "Komenca Dato",
25 | "field-label-users": "Uzantoj",
26 | "field-label-wiki": "Vikio",
27 | "field-select-placeholder": "Elekti...",
28 | "field-select-no-results": "Neniu rezulto estis trovita",
29 | "field-select-search-prompt": "Tajpi por serĉi",
30 | "made-by": "Farita de $1 de la Vikimedia Fondaĵo.",
31 | "made-by-team": "Teamo de Kontraŭ-Ĉikanaj Iloj",
32 | "privacy-policy": "Regularo pri privateco",
33 | "powered-by": "Funkcianta per Vikimedia Iloforĝejo",
34 | "report-bug": "Raporti cimon",
35 | "revision-edit-summary-removed": "forigis redaktan resumon",
36 | "view-source-code-on": "Vidi ĉe $1",
37 | "warning-no-results": "Neniu Rezulto",
38 | "discuss-on-wiki": "Diskonigi tiujn rezultojn",
39 | "discuss-on-wiki-text": "Inter $1 kaj $2, ĉe $3, [[User:$4]] ([[User talk:$4|diskutpaĝo]] | [[Special:Contributions/$4|kontribuoj]]) kaj [[User:$5]] ([[User talk:$5|diskutpaĝo]] | [[Special:Contributions/$5|kontribuoj]]) redaktis iujn paĝojn komune. Vi povas vidi kronologian liston de iliaj redaktoj ĉe la [$6 Interaga Kronologio] aŭ tabelon ĉe la [$7 Interago-Analizilo].",
40 | "discuss-on-wiki-help": "Kopiu & algluu ĉi tiun vikitekston al envikia diskuto por diskonigi ĉi tiun Interagan Kronologion kun aliuloj",
41 | "discuss-on-wiki-copy": "Kopii",
42 | "discuss-on-wiki-copied": "Kopiita!"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/te.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Chaduvari",
5 | "Veeven"
6 | ]
7 | },
8 | "app-description": "ఇద్దరు వాడుకరులు దిద్దుబాట్లు చేసిన పేజీల్లో ఆ వాడుకరుల దిద్దుబాట్ల చరిత్ర.",
9 | "app-feedback-link": "సహాయం",
10 | "app-title": "ఇంటరాక్షన్ కాలరేఖ",
11 | "back-top": "తిరిగి పైకి",
12 | "between-edits": "దిద్దుబాట్ల మధ్య $1",
13 | "between-interactions": "ఇంటరాక్షన్ల మధ్య $1",
14 | "error": "లోపం",
15 | "error-help": "$1, ఆ తరువాత $2.",
16 | "error-help-report": "లోపాన్ని నివేదించండి",
17 | "error-help-refresh": "పేజీని తాజాకరించండి",
18 | "error-message-request-url": "అభ్యర్థన URL",
19 | "error-suppressed-diff": "ఒకటో అంతకంటే ఎక్కువో కూర్పులను అణచివేసారు కాబట్టి మీరు ఈ తేడాను చూడలేరు.",
20 | "info-no-users": "ముందు ఇద్దరు వాడుకరులను ఇవ్వండి.",
21 | "info-no-wiki": "ముందొక వికీ పేరు ఇవ్వండి.",
22 | "info-required-fields": "ముందు, ఒక వికీని, ఇద్దరు వాడుకరులనూ ఇవ్వండి.",
23 | "licensed-under": "$1 లైసెన్సుకు లోబడి",
24 | "field-label-end-date": "ముగింపు తేదీ",
25 | "field-label-start-date": "ఆరంభ తేదీ",
26 | "field-label-users": "వాడుకరులు",
27 | "field-label-wiki": "వికీ",
28 | "field-select-placeholder": "ఎంచుకోండి...",
29 | "field-select-no-results": "ఫలితాలేమీ దొరకలేదు",
30 | "field-select-search-prompt": "వెతకడానికి టైపించండి",
31 | "made-by": "వికీమీడియా ఫౌండేషను వారి $1 చేసారు.",
32 | "made-by-team": "వేధింపు వ్యతిరేక పరికరాల బృందం",
33 | "privacy-policy": "గోప్యతా విధానం",
34 | "powered-by": "వికీమీడియా టూల్ఫోర్జ్ మద్దతుతో",
35 | "report-bug": "సమస్యను నివేదించండి",
36 | "revision-edit-summary-removed": "దిద్దుబాటు సారాంశాన్ని తీసివేసారు",
37 | "view-source-code-on": "$1 లో చూడండి",
38 | "warning-no-results": "ఫలితాలు లేవు",
39 | "discuss-on-wiki": "ఈ ఫలితాలను ఇతరులకు ఇవ్వండి",
40 | "discuss-on-wiki-text": "$3 లో $1 - $2 మధ్య [[User:$4]] ([[User talk:$4|చర్చ]] | [[Special:Contributions/$4|మార్పుచేర్పులు]]), [[User:$5]] ([[User talk:$5|చర్చ]] | [[Special:Contributions/$5|మార్పుచేర్పులు]]) ఇద్దరూ కొన్ని పేజీల్లో ఉమ్మడిగా దిద్దుబాట్లు చేసారు. వాళ్ళ దిద్దుబాట్లను [$6 ఇంటరాక్షన్ కాలరేఖలో] తేదీ వారీగా చూడవచ్చు. లేదా [$7 ఇంటరాక్షన్ విశ్లేషణ]లో పట్టిక రూపంలో చూడవచ్చు.",
41 | "discuss-on-wiki-help": "ఈ వికీటెక్స్టును కాపీచేసి, వికీలో జరుగుతున్న ఏ చర్చలోనైనా పేస్టు చేసి, ఈ ఫలితాలను అందరికీ చూపెట్టవచ్చు",
42 | "discuss-on-wiki-copy": "కాపీచేయి",
43 | "discuss-on-wiki-copied": "కాపీ చేసాం!"
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/epics/query.js:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 | import qs from 'querystring';
3 | import moment from 'moment';
4 | import { replace, LOCATION_CHANGE } from 'react-router-redux';
5 | import getQueryFromLocation from 'app/utils/location-query';
6 | import Query from 'app/entities/query';
7 | import { EVENTS as QUERY_EVENTS, updateQuery, setDefaultQuery } from 'app/actions/query';
8 |
9 | export const pushQueryToLocation = ( action$, store ) => (
10 | action$.filter( ( action ) => QUERY_EVENTS.includes( action.type ) )
11 | // If there are no users and no search query, no action needs to be taken.
12 | .filter( () => !getQueryFromLocation( store.getState().router.location ).equals( store.getState().query ) )
13 | .flatMap( () => {
14 | let location = store.getState().router.location;
15 | const query = getQueryFromLocation( location ).merge( store.getState().query ).toJS();
16 |
17 | // Remove any keys that have empty values.
18 | Object.keys( query ).forEach( ( key ) => {
19 | if ( !query[ key ] || query[ key ].length === 0 ) {
20 | delete query[ key ];
21 | }
22 | } );
23 |
24 | location = {
25 | ...location,
26 | search: '?' + qs.stringify( query )
27 | };
28 |
29 | return Observable.of( replace( location ) );
30 | } )
31 | );
32 |
33 | export const setDefaultQueryOnLoad = ( action$, store ) => (
34 | action$.ofType( LOCATION_CHANGE )
35 | // Only set the default query on the initial location change.
36 | .first()
37 | // If the query is empty, set the default.
38 | .filter( () => getQueryFromLocation( store.getState().router.location ).equals( new Query() ) )
39 | // Do not update the URL on page load, wait for some other action.
40 | .flatMap( () => {
41 | const query = new Query( {
42 | startDate: moment.utc().startOf( 'day' ).subtract( 30, 'days' ).unix().toString()
43 | } );
44 |
45 | return Observable.of( setDefaultQuery( query ) );
46 | } )
47 | );
48 |
49 | export const pushLocationToQuery = ( action$, store ) => (
50 | action$.ofType( LOCATION_CHANGE )
51 | // If there are no users and no search query, no action needs to be taken.
52 | .filter( () => !getQueryFromLocation( store.getState().router.location ).equals( store.getState().query ) )
53 | .flatMap( () => Observable.of( updateQuery( getQueryFromLocation( store.getState().router.location ) ) ) )
54 | );
55 |
--------------------------------------------------------------------------------
/i18n/sl.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Eleassar"
5 | ]
6 | },
7 | "app-description": "Kronološka zgodovina dveh uporabnikov na straneh, ki sta jih urejala oba.",
8 | "app-feedback-link": "Pomoč",
9 | "app-title": "Časovnica interakcij",
10 | "back-top": "Nazaj na vrh",
11 | "between-edits": "$1 med urejanjema",
12 | "between-interactions": "$1 med interakcijama",
13 | "error": "Napaka",
14 | "error-help": "$1, nato $2.",
15 | "error-help-report": "Sporočite napako",
16 | "error-help-refresh": "osvežite stran",
17 | "error-message-request-url": "URL zahtevka",
18 | "error-suppressed-diff": "Te razlike ne morete videti, saj sta bili obe redakciji ali ena od redakcij skriti.",
19 | "info-no-users": "Za začetek navedite dva uporabnika.",
20 | "info-no-wiki": "Za začetek navedite viki.",
21 | "info-required-fields": "Za začetek navedite dva uporabnika in viki.",
22 | "licensed-under": "Licenca: $1",
23 | "field-label-end-date": "Končni datum",
24 | "field-label-start-date": "Začetni datum",
25 | "field-label-users": "Uporabnika",
26 | "field-label-wiki": "Viki",
27 | "field-select-placeholder": "Izberite ...",
28 | "field-select-no-results": "Ni najdenih zadetkov",
29 | "field-select-search-prompt": "Vnesite iskalni niz",
30 | "made-by": "Ustvarila $1 Fundacije Wikimedia.",
31 | "made-by-team": "Ekipa za orodja pred nadlegovanjem",
32 | "privacy-policy": "Pravilnik o zasebnosti",
33 | "powered-by": "Omogoča Wikimedia Toolforge",
34 | "report-bug": "Sporočite hrošča",
35 | "revision-edit-summary-removed": "povzetek urejanja odstranjen",
36 | "view-source-code-on": "Ogled na spletnem mestu $1",
37 | "warning-no-results": "Ni zadetkov",
38 | "discuss-on-wiki": "Deli zadetke",
39 | "discuss-on-wiki-text": "Med datumoma $1 in $2 sta v projektu $3 uporabnika [[User:$4|$4]] ([[User talk:$4|pogovor]] | [[Special:Contributions/$4|prispevki]]) in [[User:$5|$5]] ([[User talk:$5|pogovor]] | [[Special:Contributions/$5|prispevki]]) urejala nekatere skupne strani. Časovno urejen seznam vseh njunih urejanj si lahko ogledate na [$6 Časovnici interakcij] ali kot preglednico v [$7 Analizatorju interakcij].",
40 | "discuss-on-wiki-help": "Za deljenje teh zadetkov Časovnice interakcij z drugimi skopirajte in prilepite to besedilo na pogovorno stran vikija.",
41 | "discuss-on-wiki-copy": "Kopiraj",
42 | "discuss-on-wiki-copied": "Kopirano!"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/ce.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Исмаил Садуев",
5 | "Умар"
6 | ]
7 | },
8 | "app-description": "Шина декъашхочо агӀонаш тӀехь хийцамаш бина хронологин истори.",
9 | "app-feedback-link": "ГӀо",
10 | "app-title": "Interaction Timeline",
11 | "back-top": "ЙухагӀо хьала",
12 | "between-edits": "$1 хийцамашна йукъахь",
13 | "between-interactions": "$1 йукъаметтигашна йукъахь",
14 | "error": "ГӀалат",
15 | "error-help": "$1, цул тӀаьхьа $2.",
16 | "error-help-report": "ГӀалатех хаам бар",
17 | "error-help-refresh": "агӀо карлайаккха",
18 | "error-message-request-url": "Дехаран URL-адрес",
19 | "error-suppressed-diff": "ХӀара башхалла хьажа йиш йац хьан, хӀунда аьлча цхьаъ йа ший а верси къайлайаьккхина йу.",
20 | "info-no-users": "Дехар до, шиъ декъашхо билгалбе",
21 | "info-no-wiki": "Дехар до, анализ йан вики билгалйаккха.",
22 | "info-required-fields": "Дехар до, шиъ декъашхо а, вики билгалде",
23 | "licensed-under": "Лицензица $1",
24 | "field-label-end-date": "Чекхйолу хан",
25 | "field-label-start-date": "Йолайелла хан",
26 | "field-label-users": "Декъашхой",
27 | "field-label-wiki": "Вики",
28 | "field-select-placeholder": "Харжа…",
29 | "field-select-no-results": "ХӀумма а ца карийна",
30 | "field-select-search-prompt": "Йазде лахарна",
31 | "made-by": "$1 Викимедиа Фондан командо йина.",
32 | "made-by-team": "Хьийзорна дуьхьал команда",
33 | "privacy-policy": "Конфиденционаллин политика",
34 | "powered-by": "Wikimedia Toolforge гӀирсан тӀехь",
35 | "report-bug": "Хаийта гӀалатах",
36 | "revision-edit-summary-removed": "нисдарх лаьцна хаам дӀабаьккхина",
37 | "view-source-code-on": "$1 сайт тӀехь хьажа",
38 | "warning-no-results": "Цхьа а жамӀ дац",
39 | "discuss-on-wiki": "ХӀара жамӀаш декъа",
40 | "discuss-on-wiki-text": "$1 а, $2 а йукъахь $3 вики тӀехь ши декъашхо (в)йу [[User:$4]] ([[User talk:$4|дийц.]] | [[Special:Contributions/$4|къинхьегам]]) а, [[User:$5]] ([[User talk:$5|дийц.]]| [[Special:Contributions/$5|къинхьегам]]) хийцамаш бина цхьайолчу агӀонаш тӀехь. Хьан йиш йу хьажа массо а декъашхойн хийцамийн хронологин могӀам [$6 Interaction Timeline] тӀехь йа таблицин кепехь йолу [$7 Interaction Analyser] чохь.",
41 | "discuss-on-wiki-help": "ХӀара викитекст копеш а йина, чуйилла, кхечаьрца Interaction Timeline жамӀаш декъархьама",
42 | "discuss-on-wiki-copy": "Копийан",
43 | "discuss-on-wiki-copied": "Копийина!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/fa.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Amirsara",
5 | "Ebrahim",
6 | "Ebraminio",
7 | "Jeeputer"
8 | ]
9 | },
10 | "app-description": "تاریخچهٔ زمانبندیشده دو کاربر در صفحههایی که هر دو در آنها ویرایش داشتهاند.",
11 | "app-feedback-link": "راهنما",
12 | "app-title": "خط زمانی تعامل",
13 | "back-top": "بازگشت به بالای صفحه",
14 | "between-edits": "$1 بین ویرایشها",
15 | "between-interactions": "$1 بین تعاملها",
16 | "error": "خطا",
17 | "error-help": "$1، و سپس $2",
18 | "error-help-report": "گزارش خطا",
19 | "error-help-refresh": "تازهسازی صفحه",
20 | "error-message-request-url": "درخواست نشانی اینترنتی",
21 | "error-suppressed-diff": "شما نمیتوانید این تفاوت را ببینید. زیرا هردو یا یکی از نسخهها فرونشانی شده است.",
22 | "info-no-users": "برای شروع، لطفاً دو کاربر را مشخص کنید.",
23 | "info-no-wiki": "برای شروع، لطفاً یک ویکی را مشخص کنید.",
24 | "info-required-fields": "برای شروع، لطفاً دو کاربر و یک ویکی را مشخص کنید.",
25 | "licensed-under": "تحت پروانهٔ $1",
26 | "field-label-end-date": "تاریخ پایان",
27 | "field-label-start-date": "تاریخ شروع",
28 | "field-label-users": "کاربران",
29 | "field-label-wiki": "ویکی",
30 | "field-select-placeholder": "انتخاب…",
31 | "field-select-no-results": "نتیجهای یافت نشد",
32 | "field-select-search-prompt": "برای جستجو شروع به نوشتن کنید",
33 | "made-by": "ساختهشده توسط $1 بنیاد ویکیمدیا",
34 | "made-by-team": "تیم ابزارهای مبارزه با آزار و اذیت",
35 | "privacy-policy": "سیاست حفظ حریم خصوصی",
36 | "powered-by": "قدرتگرفته از تولفورج ویکیمدیا",
37 | "report-bug": "گزارش یک اشکال",
38 | "revision-edit-summary-removed": "خلاصهٔ ویرایش حذف شد",
39 | "view-source-code-on": "مشاهده در $1",
40 | "warning-no-results": "بدون نتیجه",
41 | "discuss-on-wiki": "اشتراکگذاری این نتایج",
42 | "discuss-on-wiki-text": "بین $1 و $2 در $3 [[کاربر:$4]] ([[بحث کاربر:$4|بحث]] | [[ویژه:مشارکتها/$4|مشارکتها]]) و [[کاربر:$5]] ([[بحث کاربر:$5|بحث]] | [[ویژه:مشارکتها/$5|مشارکتها]]) هردو در برخی صفحههای مشترک ویرایشهایی داشتهاند. میتوانید یک فهرستی زمانبندیشده از تمامی ویرایشهای آنها را در [$6 خط زمانی تعامل] یا در نمای جدول در [$7 تحلیل تعامل] ببینید.",
43 | "discuss-on-wiki-help": "برای اشتراکگذاری این نتایج در خط زمانی تعامل با دیگران، این ویکیمتن را به بحثی که در ویکی مربوطه در جریان است کپی کنید",
44 | "discuss-on-wiki-copy": "رونوشت",
45 | "discuss-on-wiki-copied": "کپی شد!"
46 | }
47 |
--------------------------------------------------------------------------------
/i18n/nb.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Jon Harald Søby",
5 | "Kingu"
6 | ]
7 | },
8 | "app-description": "Kronologisk historikk for to brukere på sider der begge har gjort redigeringer.",
9 | "app-feedback-link": "Hjelp",
10 | "app-title": "Interaksjonstidslinje",
11 | "back-top": "Tilbake til toppen",
12 | "between-edits": "$1 mellom redigeringer",
13 | "between-interactions": "$1 mellom interaksjoner",
14 | "error": "Feil",
15 | "error-help": "$1, og så $2.",
16 | "error-help-report": "Rapporter feilen",
17 | "error-help-refresh": "oppdater siden",
18 | "error-message-request-url": "Forespurt URL",
19 | "error-suppressed-diff": "Du kan ikke vise denne diffen fordi én av eller begge revisjonene har blitt sensurert.",
20 | "info-no-users": "Oppgi to brukernavn for å begynne.",
21 | "info-no-wiki": "Oppgi en wiki for å begynne.",
22 | "info-required-fields": "Oppgi to brukernavn og en wiki for å begynne.",
23 | "licensed-under": "Lisensiert under $1",
24 | "field-label-end-date": "Sluttdato",
25 | "field-label-start-date": "Startdato",
26 | "field-label-users": "Brukere",
27 | "field-label-wiki": "Wiki",
28 | "field-select-placeholder": "Velg …",
29 | "field-select-no-results": "Ingen resultater funnet",
30 | "field-select-search-prompt": "Skriv for å søke",
31 | "made-by": "Laget av Wikimedia Foundations $1.",
32 | "made-by-team": "team for anti-trakassering",
33 | "privacy-policy": "Personvernspolitikk",
34 | "powered-by": "Drives av Wikimedia Toolforge",
35 | "report-bug": "Innrapporter feil",
36 | "revision-edit-summary-removed": "redigeringsforklaring fjernet",
37 | "view-source-code-on": "Vis på $1",
38 | "warning-no-results": "Ingen resultater",
39 | "discuss-on-wiki": "Del disse resultatene",
40 | "discuss-on-wiki-text": "Mellom $1 og $2 har både [[User:$4|$4]] ([[User talk:$4|diskusjon]] | [[Special:Contributions/$4|bidrag]]) og [[User:$5|$5]] ([[User talk:$5|diskusjon]] | [[Special:Contributions/$5|bidrag]]) redigert på samme sider på $3. Du kan se en kronologisk liste over redigeringene deres på [$6 interaksjonstidslinja] eller en tabellvisning på [$7 interaksjonsanalysatoren].",
41 | "discuss-on-wiki-help": "Kopier og lim inn denne wikiteksten på en diskusjonsside på wikien for å dele disse resultatene fra interaksjonstidslinja med andre",
42 | "discuss-on-wiki-copy": "Kopier",
43 | "discuss-on-wiki-copied": "Kopiert!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/ast.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Xuacu"
5 | ]
6 | },
7 | "app-description": "Historial cronolóxicu pa dos usuarios en páxines onde dambos fixeron ediciones.",
8 | "app-feedback-link": "Ayuda",
9 | "app-title": "Interaction Timeline",
10 | "back-top": "Volver arriba",
11 | "between-edits": "$1 ente ediciones",
12 | "between-interactions": "$1 ente interacciones",
13 | "error": "Error",
14 | "error-help": "$1, y llueu $2.",
15 | "error-help-report": "Informar del error",
16 | "error-help-refresh": "recarga la páxina",
17 | "error-message-request-url": "URL de la solicitú",
18 | "error-suppressed-diff": "Nun puedes ver esti diff porque suprimióse una de les revisiones o dambes.",
19 | "info-no-users": "Apurre dos usuarios pa principiar.",
20 | "info-no-wiki": "Apurre una wiki pa principiar.",
21 | "info-required-fields": "Apurre dos usuarios y la wiki pa principiar.",
22 | "licensed-under": "Baxo la llicencia $1",
23 | "field-label-end-date": "Data final",
24 | "field-label-start-date": "Data d'entamu",
25 | "field-label-users": "Usuarios",
26 | "field-label-wiki": "Wiki",
27 | "field-select-placeholder": "Seleccionar...",
28 | "field-select-no-results": "Nun s'alcontraron resultaos",
29 | "field-select-search-prompt": "Escribe pa buscar",
30 | "made-by": "Fechu pol $1 de la Fundación Wikimedia",
31 | "made-by-team": "equipu de Ferramientes Anti-Amenaces",
32 | "privacy-policy": "Política d'intimidá",
33 | "powered-by": "Cola teunoloxía de Wikimedia Toolforge",
34 | "report-bug": "Informar d'un fallu",
35 | "revision-edit-summary-removed": "resume d'edición desaniciáu",
36 | "view-source-code-on": "Ver en $1",
37 | "warning-no-results": "Nun hai resultaos",
38 | "discuss-on-wiki": "Compartir estos resultaos",
39 | "discuss-on-wiki-text": "Ente $1 y $2, en $3, [[User:$4]] ([[User talk:$4|alderique]] | [[Special:Contributions/$4|contribs]]) y [[User:$5]] ([[User talk:$5|alderique]] | [[Special:Contributions/$5|contribs]]) editaron en delles páxines en común. Puedes ver una llista cronolóxica de toles sos ediciones nel [$6 Cronograma d'Interaición] o una vista de tabla nel [$7 Analizador d'Interaición].",
40 | "discuss-on-wiki-help": "Copia y pega esti testu wiki nun alderique de la wiki pa compartir con otros estos resultaos del Cronograma d'Interaición",
41 | "discuss-on-wiki-copy": "Copiar",
42 | "discuss-on-wiki-copied": "Copióse"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/sh.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Vlad5250"
5 | ]
6 | },
7 | "app-description": "Vrijemesljedna povijest dva korisnika na stranicama u kojima su obojica uređivali.",
8 | "app-feedback-link": "Pomoć",
9 | "app-title": "Interakcijski vrijemeslijed",
10 | "back-top": "Nazad na vrh",
11 | "between-edits": "$1 između izmjenama",
12 | "between-interactions": "$1 između interakcijama",
13 | "error": "Greška",
14 | "error-help": "$1, zatim $2.",
15 | "error-help-report": "Prijavi grešku",
16 | "error-help-refresh": "preučitaj ovu stranicu",
17 | "error-message-request-url": "URL zahtjeva",
18 | "error-suppressed-diff": "Ne možete vidjeti ovu razliku jer su jedna ili obe revizije skrivene.",
19 | "info-no-users": "Navedite dva korisnika da biste počeli.",
20 | "info-no-wiki": "Navedite wiki da biste počeli.",
21 | "info-required-fields": "Navedite dva korisnika i wiki da biste počeli.",
22 | "licensed-under": "Licencirano pod $1",
23 | "field-label-end-date": "Završni datum",
24 | "field-label-start-date": "Početni datum",
25 | "field-label-users": "Korisnici",
26 | "field-label-wiki": "Wiki",
27 | "field-select-placeholder": "Izaberite...",
28 | "field-select-no-results": "Nisam ništa pronašao",
29 | "field-select-search-prompt": "Unesite da biste pretražili",
30 | "made-by": "Napravio $1 Fondacije Wikimedia.",
31 | "made-by-team": "Tim za alate protiv uznemiravanja",
32 | "privacy-policy": "Politika privatnosti",
33 | "powered-by": "Omogućeno Wikimedijinim laboratorijama za alate",
34 | "report-bug": "Prijavi grešku",
35 | "revision-edit-summary-removed": "rezime izmjene je uklonjen",
36 | "view-source-code-on": "Vidi na $1",
37 | "warning-no-results": "Nema ishoda",
38 | "discuss-on-wiki": "Podeli ovaj ishod",
39 | "discuss-on-wiki-text": "Između $1 i $2 na $3 [[User:$4]] ([[User talk:$4|razgovor]] | [[Special:Contributions/$4|doprinosi]]) and [[User:$5]] ([[User talk:$5|razgovor]] | [[Special:Contributions/$5|doprinosi]]) oba napravila su uređivanja na jednim istim stranicama. Možete pogledati vremesledni popis svih njihovih uređivanja na [$6 Interakcijskom vremesledu] ili prikaz tabele na [$7 Analizatoru interakcije].",
40 | "discuss-on-wiki-help": "Prekopirajte ovaj wikitekst u raspravi na wikiju da biste podijelili ovaj ishod interakcijskog vremesleda i drugima",
41 | "discuss-on-wiki-copy": "Kopiraj",
42 | "discuss-on-wiki-copied": "Iskopirano!"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/anp.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Proabscorp!"
5 | ]
6 | },
7 | "app-description": "उ सिनी पृष्ठ प दू उपयोगकर्ता क कालानुक्रमिक इतिहास जहां हुनी दोनो रौ संपादन करलौ छै।",
8 | "app-feedback-link": "सहायता",
9 | "app-title": "सहभागिता समयरेखा",
10 | "back-top": "घुरी क फेनु शीर्ष प जा",
11 | "between-edits": "संपादन केरौ बीच $1",
12 | "between-interactions": "सहभागिता केरौ बीच $1",
13 | "error": "त्रुटि",
14 | "error-help": "$1, आरो फिर $2.",
15 | "error-help-report": "त्रुटि रिपोर्ट करौ",
16 | "error-help-refresh": "पेज क रिफ्रेश करौ",
17 | "error-message-request-url": "URL अनुरोध करौ",
18 | "error-suppressed-diff": "तोंय हैय अंतर क नाय देखै सकै छौ केन्हेकि एगो या दूगो संशोधन रोकी देलौ गेलौ छौं।",
19 | "info-no-users": "आरंभ करै लेली कृपया दूगो उपयोगकर्ता प्रदान करौ।",
20 | "info-no-wiki": "आरम्भ करै लेली एगो विकि प्रदान करौ।",
21 | "info-required-fields": "आरम्भ करै लेली कृपया दूगो उपयोगकर्ता आरो एगो विकि प्रदान करौ।",
22 | "licensed-under": "$1 केरौ तहत लाइसेंस प्राप्त छै",
23 | "field-label-end-date": "समाप्ति तिथि",
24 | "field-label-start-date": "प्रारम्भ तिथि",
25 | "field-label-users": "सदस्य",
26 | "field-label-wiki": "विकि",
27 | "field-select-placeholder": "चयन करौ...",
28 | "field-select-no-results": "कोय परिणाम नै मिललै",
29 | "field-select-search-prompt": "खोजै लेली लिखौ",
30 | "made-by": "विकिमीडिया फाउंडेशन केरौ $1 द्वारा निर्मित।",
31 | "made-by-team": "उत्पीड़न-विरोधी साधन दल",
32 | "privacy-policy": "गोपनीयता नीति",
33 | "powered-by": "विकिमीडिया टूलफोर्ज द्वारा संचालित",
34 | "report-bug": "बग क रिपोर्ट करौ",
35 | "revision-edit-summary-removed": "सम्पादन सारांश हटलै",
36 | "view-source-code-on": "$1 प देखौ",
37 | "warning-no-results": "कोय परिणाम नाय",
38 | "discuss-on-wiki": "इ परिणाम क साँझा करौ",
39 | "discuss-on-wiki-text": "$3 प $1 आरो $2 केरौ बीच [[उपयोगकर्ता:$4]] ([[उपयोगकर्ता वार्ता:$4|वार्ता]]| [[विशेष:योगदान/$4|contribs]]) आरो [[उपयोगकर्ता:$5]] ([[उपयोगकर्ता वार्ता:$5|वार्ता]] [[विशेष:योगदान/$5|contribs]]) दोनो न कुछु पृष्ठ क आम तौर प संपादित करलकै। तोंय उनकौ सब्भे संपादन क कालानुक्रमिक सूची [$6 सहभागिता समयरेखा] या [$7 सहभागिता समयरेखा विश्लेषक] प एगो तालिका केरौ रूप मँ देखै सकै छौ।",
40 | "discuss-on-wiki-help": "दोसरौ केरौ साथ इ सहभागिता समयरेखा परिणाम क साँझा करै लेली हैय विकिपाठ क एगो विकि चर्चा प कॉपी आरो पेस्ट करौ",
41 | "discuss-on-wiki-copy": "प्रतिलिपि बनाबौ",
42 | "discuss-on-wiki-copied": "प्रतिलिपि बनैलौ गेलै!"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/diq.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "1917 Ekim Devrimi",
5 | "Mirzali",
6 | "Orbot707"
7 | ]
8 | },
9 | "app-description": "Kronolociye heta dı karberan rê zi herg pele dı ca ca verorê pela",
10 | "app-feedback-link": "Peşti",
11 | "app-title": "Diyagramê yewbinibandurnayışi",
12 | "back-top": "Şo serêniye",
13 | "between-edits": "Mabeynê vurriyayışan $1",
14 | "between-interactions": "Yewbinibandurnayış miyan de $1",
15 | "error": "Xeta",
16 | "error-help": "$1, û dıma $2.",
17 | "error-help-report": "Xeta rapor ke",
18 | "error-help-refresh": "pele newe ke",
19 | "error-message-request-url": "Waşte URL",
20 | "error-suppressed-diff": "Şıma nêşenê qet yew ferq bıvênê, çıke vurriyayışi hurdemêna zi dariyê we.",
21 | "info-no-users": "Kerem ke, seba destpêkerdış rê dı karberan temin ke.",
22 | "info-no-wiki": "Kerem ke, yew wiki xo rê weçine.",
23 | "info-required-fields": "Kerem ke, seba destpêkerdış rê dı karberan û wiki temin ke.",
24 | "licensed-under": "Bınê $1 de lisans kerdo",
25 | "field-label-end-date": "Tarixê qediyayışi",
26 | "field-label-start-date": "Tarixê destpêkerdışi",
27 | "field-label-users": "Karberi",
28 | "field-label-wiki": "Wiki",
29 | "field-select-placeholder": "Weçine...",
30 | "field-select-no-results": "Neticeyi nêvêniyayi",
31 | "field-select-search-prompt": "Tipê cıgeyrayışi",
32 | "made-by": "Terefê Weqfê Wikimedya $1 ra biyo.",
33 | "made-by-team": "Taxımê hacetanê anti-tecizi",
34 | "privacy-policy": "Politikaya nımıtiye",
35 | "powered-by": "Terefê Wikimedya Toolforge ra peşti vêniyena",
36 | "report-bug": "Xeta bırışe",
37 | "revision-edit-summary-removed": "xulasaya vurriyayışi esteriye",
38 | "view-source-code-on": "$1 de bıvêne",
39 | "warning-no-results": "Neticeyi çıniyê",
40 | "discuss-on-wiki": "Enê neticeyan vıla ke",
41 | "discuss-on-wiki-text": "Miyanê $1 u $2 de $3 [[User:$4]] ([[User talk:$4|mesac]] | [[Special:Contributions/$4|iştiraki]]) u [[User:$5]] ([[User talk:$5|mesac]] | [[Special:Contributions/$5|iştiraki]]) wikiyan de hurdemêna zi zey yewbini vurnayışi kerdê. [$6 xeta zemaniya tesiri] de vurnayışanê pêroyıne ya zi [$7 agoznayışê tesiri] de tabloyo kronolocik de pêro asenê.",
42 | "discuss-on-wiki-help": "Nê neticeyanê tabloy zemanê tesiri tayêna merdımana vıla kerdışi rê nê wikimetini kopya kerê u pronê.",
43 | "discuss-on-wiki-copy": "Kopya ke",
44 | "discuss-on-wiki-copied": "Kopya biyo!"
45 | }
46 |
--------------------------------------------------------------------------------
/i18n/sv.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Maol",
5 | "Sabelöga",
6 | "WikiPhoenix"
7 | ]
8 | },
9 | "app-description": "Kronologisk historik för två användare på sidor som de båda har redigerat.",
10 | "app-feedback-link": "Hjälp",
11 | "app-title": "Interaktionstidslinje",
12 | "back-top": "Tillbaka till toppen",
13 | "between-edits": "$1 mellan redigeringarna",
14 | "between-interactions": "$1 mellan interaktioner",
15 | "error": "Fel",
16 | "error-help": "$1 och sedan $2.",
17 | "error-help-report": "Rapportera felet",
18 | "error-help-refresh": "uppdatera sidan",
19 | "error-message-request-url": "Begär webbadress",
20 | "error-suppressed-diff": "Du kan inte se denna skillnad eftersom en eller båda sidversioner har censurerats.",
21 | "info-no-users": "Ange två användare för att börja.",
22 | "info-no-wiki": "Ange en wiki för att börja.",
23 | "info-required-fields": "Ange två användare och wiki för att börja.",
24 | "licensed-under": "Licensierad under $1",
25 | "field-label-end-date": "Slutdatum",
26 | "field-label-start-date": "Startdatum",
27 | "field-label-users": "Användare",
28 | "field-label-wiki": "Wiki",
29 | "field-select-placeholder": "Välj...",
30 | "field-select-no-results": "Inga resultat hittades",
31 | "field-select-search-prompt": "Skriv för att söka",
32 | "made-by": "Skapad av Wikimedia Foundations $1.",
33 | "made-by-team": "Antimobbningsverktygsgruppen",
34 | "privacy-policy": "Integritetspolicy",
35 | "powered-by": "Drivs av Wikimedia Toolforge",
36 | "report-bug": "Rapportera ett fel",
37 | "revision-edit-summary-removed": "redigeringssammanfattning borttagen",
38 | "view-source-code-on": "Visa på $1",
39 | "warning-no-results": "Inga resultat",
40 | "discuss-on-wiki": "Dela dessa resultat",
41 | "discuss-on-wiki-text": "Mellan $1 och $2 på $3 [[User:$4]] ([[User talk:$4|diskussion]] | [[Special:Contributions/$4|bidrag]]) och [[User:$5]] ([[User talk:$5|diskussion]] | [[Special:Contributions/$5|bidrag]]) redigerade båda några gemensamma sidor. Du kan se en kronologisk lista över alla deras redigeringar på [$6 interaktionstidslinjen] eller en tabellvy över [$7 interaktionsanalyseraren].",
42 | "discuss-on-wiki-help": "Kopiera och klistra in denna wikitext i en annan wikidiskussion för att dela resultaten från interaktionstidslinjen med andra",
43 | "discuss-on-wiki-copy": "Kopiera",
44 | "discuss-on-wiki-copied": "Kopierades!"
45 | }
46 |
--------------------------------------------------------------------------------
/i18n/jv-java.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Bennylin"
5 | ]
6 | },
7 | "app-description": "Riwayat kronologis kanggo rong panganggo ing kaca sing wis diowahi loro-lorone.",
8 | "app-feedback-link": "ꦥꦶꦠꦸꦭꦸꦁ",
9 | "app-title": "ꦭꦶꦤꦶꦮꦼꦏ꧀ꦠꦸꦆꦤ꧀ꦠꦼꦂꦫꦏ꧀ꦱꦶ",
10 | "back-top": "ꦧꦭꦶꦚꦁꦤ꧀ꦝꦸꦮꦸꦂ",
11 | "between-edits": "$1ꦲꦤ꧀ꦠꦫꦤꦺꦧꦼꦱꦸꦠ꧀ꦠꦤ꧀",
12 | "between-interactions": "$1ꦲꦤ꧀ꦠꦫꦤꦺꦆꦤ꧀ꦠꦼꦂꦫꦏ꧀ꦱꦶ",
13 | "error": "ꦒꦭꦠ꧀",
14 | "error-help": "$1꧈ꦧꦚ꧀ꦗꦸꦂ$2꧉",
15 | "error-help-report": "ꦭꦥꦺꦴꦂꦫꦏꦺꦩꦱꦭꦃ",
16 | "error-help-refresh": "ꦲꦚꦂꦫꦶꦏꦕ",
17 | "error-message-request-url": "ꦗꦭꦸꦏ꧀URL",
18 | "error-suppressed-diff": "Sampéyan ora bisa ndeleng bédané rèvisi iki amarga salah siji utawa loro-loroné rèvisi wis disingkiraké.",
19 | "info-no-users": "Mangga paringana kalih panganggo kanggé miwiti.",
20 | "info-no-wiki": "Mangga paringana satunggal wiki kanggé miwiti.",
21 | "info-required-fields": "Mangga paringana kalih panganggo lan satunggal wiki kanggé miwiti.",
22 | "licensed-under": "ꦢꦶꦭꦶꦱꦺꦤ꧀ꦱꦶꦏ꧀ꦤꦺꦱꦗꦿꦺꦴꦤꦶꦁ$1",
23 | "field-label-end-date": "ꦠꦁꦒꦭ꧀ꦧꦸꦧꦂ",
24 | "field-label-start-date": "ꦠꦁꦒꦭ꧀ꦮꦶꦮꦶꦠ꧀",
25 | "field-label-users": "ꦤꦫꦒꦸꦤ",
26 | "field-label-wiki": "ꦮꦶꦏꦶ",
27 | "field-select-placeholder": "ꦥꦶꦭꦶꦃ…",
28 | "field-select-no-results": "ꦮꦺꦴꦃꦲꦶꦁꦒꦺꦴꦭꦺꦏ꧀ꦲꦺꦴꦫꦲꦤ",
29 | "field-select-search-prompt": "ꦏꦼꦠꦶꦁꦏꦁꦒꦺꦴꦲꦁꦒꦺꦴꦭꦺꦏ꧀ꦏꦶ",
30 | "made-by": "ꦢꦶꦒꦮꦺꦢꦺꦤꦶꦁ$1ꦱꦏꦮꦶꦏꦶꦩꦺꦢꦶꦪꦃ",
31 | "made-by-team": "ꦠꦶꦩ꧀Anti-Harassment Tools",
32 | "privacy-policy": "ꦤꦶꦠꦶꦥꦿꦶꦮ꦳ꦱꦶ",
33 | "powered-by": "ꦢꦶꦢꦸꦏꦸꦁꦢꦺꦤꦶꦁWikimedia Toolforge",
34 | "report-bug": "ꦭꦥꦺꦴꦂꦏꦼꦏꦸꦠꦸ",
35 | "revision-edit-summary-removed": "ꦫꦶꦁꦏꦼꦱ꧀ꦱꦤ꧀ꦧꦼꦱꦸꦠ꧀ꦠꦤ꧀ꦢꦶꦲꦥꦸꦱ꧀",
36 | "view-source-code-on": "ꦢꦼꦊꦁꦲꦶꦁ$1",
37 | "warning-no-results": "ꦲꦺꦴꦫꦲꦤꦏꦱꦶꦭ꧀",
38 | "discuss-on-wiki": "ꦧꦒꦺꦏ꧀ꦤꦺꦏꦱꦶꦭ꧀ꦲꦶꦏꦶ",
39 | "discuss-on-wiki-text": "Antara $1 dan $2 pada $3 [[Pengguna:$4]] ([[Pembicaraan Pengguna:$4|bicara]] | [[Istimewa:Kontribusi/$4|kontribusi]]) dan [[Pengguna:$5]] ([[Pembicaraan Pengguna:$5|bicara]] | [[Istimewa:Kontribusi/$5|contribs]]) keduanya menyunting halaman tertentu. Anda dapat melihat daftar kronologis semua penyuntingan mereka pada [$6 Garis Waktu Interaksi] atau daftar tabel pada or [$7 Garis Waktu Interaksi].",
40 | "discuss-on-wiki-help": "Salin & tempelkan wikiteks ini ke diskusi pada wiki untuk membagikan hasil Garis Waktu Interaksi dengan orang lain",
41 | "discuss-on-wiki-copy": "ꦱꦭꦶꦤ꧀",
42 | "discuss-on-wiki-copied": "ꦢꦶꦱꦭꦶꦤ꧀!"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/sr-ec.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Aca",
5 | "BadDog",
6 | "Obsuser"
7 | ]
8 | },
9 | "app-description": "Хронолошка историја за два корисника на страницама где су обојица направила измене.",
10 | "app-feedback-link": "Помоћ",
11 | "app-title": "Хронологија интеракције",
12 | "back-top": "Назад на врх",
13 | "between-edits": "$1 између измена",
14 | "between-interactions": "$1 између интеракција",
15 | "error": "Грешка",
16 | "error-help": "$1, а онда $2.",
17 | "error-help-report": "Пријавите грешку",
18 | "error-help-refresh": "освежите страницу",
19 | "error-message-request-url": "Захтевајте URL",
20 | "error-suppressed-diff": "Не можете да видите ову разлику јер је једна или обе измене сакривене.",
21 | "info-no-users": "Наведите два корисника да бите почели.",
22 | "info-no-wiki": "Наведите вики да бисте почели.",
23 | "info-required-fields": "Наведите два корисника и вики да бисте почели.",
24 | "licensed-under": "Лиценцирано под $1",
25 | "field-label-end-date": "Датум завршетка",
26 | "field-label-start-date": "Датум почетка",
27 | "field-label-users": "Корисници",
28 | "field-label-wiki": "Вики",
29 | "field-select-placeholder": "Изаберите…",
30 | "field-select-no-results": "Резултати нису пронађени",
31 | "field-select-search-prompt": "Откуцајте да бисте претражили",
32 | "made-by": "Направио $1 Фондације Викимедија.",
33 | "made-by-team": "Тим алатке за борбу против узнемиравања",
34 | "privacy-policy": "Политика приватности",
35 | "powered-by": "Покреће Wikimedia Toolforge",
36 | "report-bug": "Пријавите грешку",
37 | "revision-edit-summary-removed": "резиме измене је уклоњен",
38 | "view-source-code-on": "Прикажи на пројекту $1",
39 | "warning-no-results": "Нема резултата",
40 | "discuss-on-wiki": "Подели ове резултате",
41 | "discuss-on-wiki-text": "Између $1 и $2 на $3 [[User:$4]] ([[User talk:$4|talk]] | [[Special:Contributions/$4|contribs]]) и [[User:$5]] ([[User talk:$5|talk]] | [[Special:Contributions/$5|contribs]]) оба су направила измене на неким заједничким страницама. Можете да видете хронолошку листу свих њихових измена на [$6 Хронологији интеракције] или приказ табеле на [$7 Анализатору интеракције].",
42 | "discuss-on-wiki-help": "Копирајте и налепите овај викитекст на дискусију на викију да бисте делили ове резултате Хронологије интеракције са другима",
43 | "discuss-on-wiki-copy": "Копирај",
44 | "discuss-on-wiki-copied": "Копирано!"
45 | }
46 |
--------------------------------------------------------------------------------
/i18n/da.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Joedalton",
5 | "Saederup92"
6 | ]
7 | },
8 | "app-description": "Kronologisk historik for to brugere på sider, hvor de begge har lavet redigeringer.",
9 | "app-feedback-link": "Hjælp",
10 | "app-title": "Interaktionstidslinje",
11 | "back-top": "Tilbage til toppen",
12 | "between-edits": "$1 mellem redigeringer",
13 | "between-interactions": "$1 mellem interaktioner",
14 | "error": "Fejl",
15 | "error-help": "$1, og så $2.",
16 | "error-help-report": "Rapporter fejlen",
17 | "error-help-refresh": "opdater siden",
18 | "error-message-request-url": "Anmod om adresse",
19 | "error-suppressed-diff": "Du kan ikke se forskellen da en eller begge af revisionerne er blevet undertrykt.",
20 | "info-no-users": "Angiv venligst to brugere for at begynde.",
21 | "info-no-wiki": "Angiv venligst en wiki for at begynde.",
22 | "info-required-fields": "Angiv venligst to brugere og en wiki for at begynde.",
23 | "licensed-under": "Licenseret under $1",
24 | "field-label-end-date": "Slutdato",
25 | "field-label-start-date": "Startdato",
26 | "field-label-users": "Brugere",
27 | "field-label-wiki": "Wiki",
28 | "field-select-placeholder": "Vælg ...",
29 | "field-select-no-results": "Ingen resultater fundet",
30 | "field-select-search-prompt": "Tast for at søge",
31 | "made-by": "Lavet af Wikimedia Foundations $1.",
32 | "made-by-team": "Anti-Harassment Tools-holdet",
33 | "privacy-policy": "Behandling af personlige oplysninger",
34 | "powered-by": "Drevet af Wikimedia Toolforge",
35 | "report-bug": "Rapporter en fejl",
36 | "revision-edit-summary-removed": "redigeringsbeskrivelsen er fjernet",
37 | "view-source-code-on": "Vis på $1",
38 | "warning-no-results": "Ingen resultater",
39 | "discuss-on-wiki": "Del disse resultater",
40 | "discuss-on-wiki-text": "Mellem $1 og $2 den $3 [[User:$4]] ([[User talk:$4|talk]] | [[Special:Contributions/$4|contribs]]) og [[User:$5]] ([[User talk:$5|talk]] | [[Special:Contributions/$5|contribs]]) foretog begge redigeringer på nogle fælles sider. Du kan se en kronologisk liste over alle deres redigeringer på [$6 Interaction Timeline] eller en tabelvisning på [$7 Interaction Analyser].",
41 | "discuss-on-wiki-help": "Kopier og indsæt denne wikitekst til en diskussion på wiki for at dele disse tidslinjeresultater vedrørende interaktion med andre",
42 | "discuss-on-wiki-copy": "Kopier",
43 | "discuss-on-wiki-copied": "Kopieret!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/fi.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Nike",
5 | "Pyscowicz"
6 | ]
7 | },
8 | "app-description": "Kronologinen historia kahdelle käyttäjälle sivuilla, missä molemmat ovat tehneet muokkauksia.",
9 | "app-feedback-link": "Ohje",
10 | "app-title": "Vuorovaikutteinen aikajana",
11 | "back-top": "Takaisin alkuun",
12 | "between-edits": "$1 muokkausten välillä",
13 | "between-interactions": "$1 aktiviteettien välillä",
14 | "error": "Virhe",
15 | "error-help": "$1, ja sitten $2.",
16 | "error-help-report": "Ilmoita virheestä",
17 | "error-help-refresh": "päivitä sivu",
18 | "error-message-request-url": "Pyynnön URL",
19 | "error-suppressed-diff": "Et voi tarkastella tätä eroa, koska yksi tai useampi muokkaushistoria on häivytetty.",
20 | "info-no-users": "Anna kaksi käyttäjää aloittaaksesi.",
21 | "info-no-wiki": "Anna wiki aloittaaksesi.",
22 | "info-required-fields": "Anna kaksi käyttäjää ja wiki aloittaaksesi.",
23 | "licensed-under": "Lisensoitu lisenssillä $1",
24 | "field-label-end-date": "Päättymispäivä",
25 | "field-label-start-date": "Alkamispäivä",
26 | "field-label-users": "Käyttäjät",
27 | "field-label-wiki": "Wiki",
28 | "field-select-placeholder": "Valitse...",
29 | "field-select-no-results": "Tuloksia ei löytynyt",
30 | "field-select-search-prompt": "Kirjoita hakeaksesi",
31 | "made-by": "Tehnyt Wikimedia Foundationin $1",
32 | "made-by-team": "Anti-Harassment Tools -tiimi",
33 | "privacy-policy": "Tietosuojakäytäntö",
34 | "powered-by": "Palvelun tarjoaa Wikimedia Toolforge",
35 | "report-bug": "Ilmoita viasta",
36 | "revision-edit-summary-removed": "muokkausyhteenveto poistettu",
37 | "view-source-code-on": "Näytä $1issa",
38 | "warning-no-results": "Ei tuloksia",
39 | "discuss-on-wiki": "Jaa nämä tulokset",
40 | "discuss-on-wiki-text": "$1 ja $2 välillä sivuston $3 käyttäjät [[User:$4]] ([[User talk:$4|keskustelusivu]] | [[Special:Contributions/$4|muokkaukset]]) ja [[User:$5]] ([[User talk:$5|keskustelusivu]] | [[Special:Contributions/$5|muokkaukset]]) tekivät muutoksia joihinkin samoihin sivuihin. Voit nähdä luettelon heidän muokkauksista aikajärjestyksessä [$6 vuorovaikutusaikajanalla] tai taulukkomuodossa [$7 vuorovaikutusanalysaattorissa].",
41 | "discuss-on-wiki-help": "Kopioi ja liitä tämä wikiteksti wikissä olevaan keskusteluun jakaaksesi nämä vuorovaikutusaikajanan tulokset muiden kanssa",
42 | "discuss-on-wiki-copy": "Kopioi",
43 | "discuss-on-wiki-copied": "Kopioitu!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/hi.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Abijeet Patro",
5 | "Shypoetess"
6 | ]
7 | },
8 | "app-description": "उन पृष्ठों पर दो उपयोगकर्ताओं के लिए कालक्रमबद्ध इतिहास जहाँ उन दोनों ने संपादन किए हैं।",
9 | "app-feedback-link": "सहायता",
10 | "app-title": "सहभागिता समयरेखा",
11 | "back-top": "वापस शीर्ष पे जाएँ",
12 | "between-edits": "संपादनों के बीच $1",
13 | "between-interactions": "सहभागिता के बीच $1",
14 | "error": "त्रुटि",
15 | "error-help": "$1, और फिर $2.",
16 | "error-help-report": "त्रुटि रिपोर्ट करें",
17 | "error-help-refresh": "पेज को रिफ्रेश करें",
18 | "error-message-request-url": "URL अनुरोध करें",
19 | "error-suppressed-diff": "आप इस अंतर को नहीं देख सकते क्योंकि एक या दोनों संशोधन रोक दिए गए हैं।",
20 | "info-no-users": "आरंभ करने के लिए कृपया दो उपयोगकर्ता प्रदान करें।",
21 | "info-no-wiki": "आरम्भ करने के लिए एक विकि प्रदान करें।",
22 | "info-required-fields": "आरम्भ करने के लिए कृपया दो उपयोगकर्ता और एक विकि प्रदान करें।",
23 | "licensed-under": "$1 के तहत लाइसेंस प्राप्त है",
24 | "field-label-end-date": "समाप्ति तिथि",
25 | "field-label-start-date": "प्रारम्भ तिथि",
26 | "field-label-users": "सदस्य",
27 | "field-label-wiki": "विकि",
28 | "field-select-placeholder": "चयन करें...",
29 | "field-select-no-results": "कोई परिणाम नहीं मिले",
30 | "field-select-search-prompt": "खोजने के लिए लिखें",
31 | "made-by": "विकिमीडिया फाउंडेशन के $1 द्वारा निर्मित।",
32 | "made-by-team": "उत्पीड़न-विरोधी साधन दल",
33 | "privacy-policy": "गोपनीयता नीति",
34 | "powered-by": "विकिमीडिया टूलफोर्ज द्वारा संचालित",
35 | "report-bug": "त्रुटि को सूचित करें",
36 | "revision-edit-summary-removed": "सम्पादन सारांश हटाया",
37 | "view-source-code-on": "$1 पर देखें",
38 | "warning-no-results": "कोई परिणाम नहीं",
39 | "discuss-on-wiki": "इन परिणामों को साँझा करें",
40 | "discuss-on-wiki-text": "$3 पर $1 और $2 के बीच [[उपयोगकर्ता:$4]] ([[उपयोगकर्ता वार्ता:$4|वार्ता]]| [[विशेष:योगदान/$4|contribs]]) और [[उपयोगकर्ता:$5]] ([[उपयोगकर्ता वार्ता:$5|वार्ता]] [[विशेष:योगदान/$5|contribs]]) दोनों ने कुछ पृष्ठों को आम तौर पर संपादित किया। आप उनके सभी संपादनों की कालानुक्रमिक सूची [$6 सहभागिता समयरेखा] या [$7 सहभागिता समयरेखा विश्लेषक] पर एक तालिका के रूप में देख सकते हैं।",
41 | "discuss-on-wiki-help": "दूसरों के साथ इन सहभागिता समयरेखा परिणामों को साँझा करने के लिए इस विकिपाठ को एक विकि चर्चा पर कॉपी और पेस्ट करें",
42 | "discuss-on-wiki-copy": "प्रतिलिपि बनाएँ",
43 | "discuss-on-wiki-copied": "प्रतिलिपि बनाई गई!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/krc.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Къарачайлы"
5 | ]
6 | },
7 | "app-description": "Экиси да тюзелтиу этген бетледе эки къошулуучу ючюн хронология тарих.",
8 | "app-feedback-link": "Болушлукъ",
9 | "app-title": "Интеракцияны хронологиясы",
10 | "back-top": "Башына къайт",
11 | "between-edits": "Тюзетиуле арасында $1",
12 | "between-interactions": "Бир бири бла байламлыкъ арасында $1",
13 | "error": "Халат",
14 | "error-help": "$1, сора $2.",
15 | "error-help-report": "Халатны билдир",
16 | "error-help-refresh": "бетни джангырт",
17 | "error-message-request-url": "URL излем",
18 | "error-suppressed-diff": "Версияларындан бири неда экиси да букъдурулдугъу ючюн, бу башхалыкъны кёраллмазсыз.",
19 | "info-no-users": "Тилейбиз, башлар ючюн эки къошулуучуну белгилегиз.",
20 | "info-no-wiki": "Тилейбиз, башлар ючюн викини белгилегиз.",
21 | "info-required-fields": "Тилейбиз, башлар ючюн эки къошулуучуну эмда эки вики белгилегиз.",
22 | "licensed-under": "$1 тюбюнде лицензиялы",
23 | "field-label-end-date": "Бошалыу дата",
24 | "field-label-start-date": "Башлау дата",
25 | "field-label-users": "Къошулуучула",
26 | "field-label-wiki": "Вики",
27 | "field-select-placeholder": "Сайла...",
28 | "field-select-no-results": "Джукъ да табылмады",
29 | "field-select-search-prompt": "Излер ючюн джазыгъыз",
30 | "made-by": "Викимедия Фондну $1 командасы этгенди.",
31 | "made-by-team": "Анти-Зорлукъ Адырланы командасы",
32 | "privacy-policy": "Ташалыкъ политика",
33 | "powered-by": "Wikimedia Toolforge дагъан",
34 | "report-bug": "Халатны юсюнден билдир",
35 | "revision-edit-summary-removed": "тюрлендириуню суратлауу кетерилгенди",
36 | "view-source-code-on": "$1 сайтда къара",
37 | "warning-no-results": "Эсеб джокъду",
38 | "discuss-on-wiki": "Бу эсеблени юлеш",
39 | "discuss-on-wiki-text": "$1 бла $2 арасында $3 юсюнден [[User:$4]] ([[User talk:$4|билдириу]] | [[Special:Contributions/$4|къошуу]]) эмда [[User:$5]] ([[User talk:$5|билдириу]] | [[Special:Contributions/$5|къошуу]]) викилеге хар экиси да ортакъ бир къауум бетледе тюзетиу этди. [$6 Interaction Timeline]-да бютеу тюзетиулени хронология тизмесин неда'[$7 Interaction Analyser]-де таблица кёрюнюмню кёрюрге боллукъсуз.",
40 | "discuss-on-wiki-help": "Бу Бир-бирине Байламлыкъ Хронологияны эсеблерин башхала бла юлешир ючюн бу викитекстни викисюзюуге копия этиб салыгъыз.",
41 | "discuss-on-wiki-copy": "Копия эт",
42 | "discuss-on-wiki-copied": "Копия этилди!"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/be-tarask.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Red Winged Duck"
5 | ]
6 | },
7 | "app-description": "Храналягічная гісторыя для двух удзельнікаў на старонках, якія яны абодва рэдагавалі",
8 | "app-feedback-link": "Дапамога",
9 | "app-title": "Часавая шкала ўзаемадзеяньня",
10 | "back-top": "Вярнуцца ўгару",
11 | "between-edits": "$1 паміж зьменамі",
12 | "between-interactions": "$1 паміж узаемадзеяньнямі",
13 | "error": "Памылка",
14 | "error-help": "$1, а потым $2.",
15 | "error-help-report": "Паведаміць пра памылку",
16 | "error-help-refresh": "абнавіць старонку",
17 | "error-message-request-url": "URL запыту",
18 | "error-suppressed-diff": "Вы ня можаце праглядзець гэтую розьніцу, бо адна ці абедзьве вэрсіі былі схаваныя.",
19 | "info-no-users": "Калі ласка, для пачатку падайце імёны двух удзельнікаў.",
20 | "info-no-wiki": "Калі ласка, падайце вікі для пачатку.",
21 | "info-required-fields": "Калі ласка, падайце імёны двух удзельнікаў і вікі для пачатку.",
22 | "licensed-under": "Паводле ліцэнзіі $1",
23 | "field-label-end-date": "Дата сканчэньня",
24 | "field-label-start-date": "Дата пачатку",
25 | "field-label-users": "Удзельнікі",
26 | "field-label-wiki": "Вікі",
27 | "field-select-placeholder": "Абярыце…",
28 | "field-select-no-results": "Нічога ня знойдзена",
29 | "field-select-search-prompt": "Увядзіце для пошуку",
30 | "made-by": "Зроблена камандай $1 фундацыі «Вікімэдыя».",
31 | "made-by-team": "Каманда барацьбы зь перасьледам",
32 | "privacy-policy": "Палітыка прыватнасьці",
33 | "powered-by": "На плятформе «Вікімэдыі» Toolforge",
34 | "report-bug": "Паведаміць пра памылку",
35 | "revision-edit-summary-removed": "апісаньне рэдагаваньня выдаленае",
36 | "view-source-code-on": "Паглядзець на $1",
37 | "warning-no-results": "Няма вынікаў",
38 | "discuss-on-wiki": "Падзяліцца гэтымі вынікамі",
39 | "discuss-on-wiki-text": "Паміж $1 і $2 на $3 [[User:$4]] ([[User talk:$4|гутаркі]] | [[Special:Contributions/$4|унёсак]]) і [[User:$5]] ([[User talk:$5|гутаркі]] | [[Special:Contributions/$5|унёсак]]) абодва рабілі праўкі на агульных старонках. Вы можаце бачыць храналягічны сьпіс іх правак у [$6 Часавай шкале ўзаемадзеяньня] ці ў выглядзе табліцы ў [$7 Аналізатары ўзаемадзеяньня].",
40 | "discuss-on-wiki-help": "Скапіюйце і ўстаўце гэты вікітэкст ў дыскусію ў вікі, каб падзяліцца вынікамі гэтай часавай шкалы зь іншымі",
41 | "discuss-on-wiki-copy": "Капіяваць",
42 | "discuss-on-wiki-copied": "Скапіявана!"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/hyw.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Armeniki",
5 | "Mutante"
6 | ]
7 | },
8 | "app-description": "Ժամանակագրական պատմութիւնը երկու գործածողներու համար, որոնք նոյն էջը խմբագրած էն:",
9 | "app-feedback-link": "Օգնութիւն",
10 | "app-title": "Միջնարարութեան ժամանակացոյց",
11 | "back-top": "Վերադառնալ դէպի էջին սկիզբը:",
12 | "between-edits": "$1 խմբագրութիւններու տակ",
13 | "between-interactions": "$1 հատ միջնարարութիւններու միջեւ",
14 | "error": "Սխալ",
15 | "error-help": "$1, եւ յետոյ $2.",
16 | "error-help-report": "Տեղեկացնել մոլորութիւնը",
17 | "error-help-refresh": "թարմացնել այս էջը",
18 | "error-message-request-url": "Պահանջել ԵԱԳ-ը",
19 | "error-suppressed-diff": "Այս էջի տարբերութիւնները չէք կրնալ նայիլ որովհետեւ մէկ (կամ երկու) վերաքննութիւններ կան որ ընկճուած են:",
20 | "info-no-users": "Հաճեցէք հայթայթել երկու գործածողներ եւ ուիքի մը սկսելու համար:",
21 | "info-no-wiki": "Հաճեցէք ուիքի մը հայթայթել սկսելու համար:",
22 | "info-required-fields": "Հաճեցէք հայթայթել երկու գործածողներ եւ ուիքի մը սկսելու համար:",
23 | "licensed-under": "Հրամանագրուած է $1-ի տակ",
24 | "field-label-end-date": "Վերջնական Թուական",
25 | "field-label-start-date": "Սկզբնաւորութեան Թուական",
26 | "field-label-users": "Գործածողներ",
27 | "field-label-wiki": "Ուիքի",
28 | "field-select-placeholder": "Ընտրէ՛",
29 | "field-select-no-results": "Հետեւանքներ չը գտնուեցան",
30 | "field-select-search-prompt": "Գրէք որոնելու համար...",
31 | "made-by": "Ստեղծուած է Wikimedia Foundation-ի $1:",
32 | "made-by-team": "Հակա-Ոտնձգութեան Գործիքի խումբ",
33 | "privacy-policy": "Մտերմութեան ապահովագիր",
34 | "powered-by": "«Ուիքիմետիա Թուլֆորճի» կարողութիւնով",
35 | "report-bug": "Մոլորութիւնի մասին տեղեկացնել",
36 | "revision-edit-summary-removed": "Խմբագրութեան ամփոփումը ջնջուած է",
37 | "view-source-code-on": "$1-ին վրայ նայիլ",
38 | "warning-no-results": "Հետեւանք չկայ",
39 | "discuss-on-wiki": "Բաժնել այս հետեւանքները",
40 | "discuss-on-wiki-text": "$1- մինչեւ $2 թուականին, եւ $3 թուականին, [[User:$4]] ([[User talk:$4|talk]] | [[Special:Contributions/$4|contribs]]) եւ [[User:$5]] ([[User talk:$5|talk]] | [[Special:Contributions/$5|contribs]]) փոփոխութիւններ կատարեցին քանի մը էջերու վրայ: Կարելի է տեսնել ժամանակախօսական ցանկ մը որ կը բովանդակէ ամէն խմբագրութիւնները, [$6 Interaction Timeline]-ի վրայ կամ [$7 Interaction Analyser] աղիւսակ-ի վրայ:",
41 | "discuss-on-wiki-copy": "Պատճէնել",
42 | "discuss-on-wiki-copied": "Պատճէնուեցաւ:"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/tl.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "GinawaSaHapon",
5 | "Jojit fb"
6 | ]
7 | },
8 | "app-description": "Kronolohikal na nakaraan ng dalawang tagagamit sa mga pahinang pareho nilang in-edit.",
9 | "app-feedback-link": "Tulong",
10 | "app-title": "Interaction Timeline",
11 | "back-top": "Umakyat",
12 | "between-edits": "$1 sa pagitan ng mga binago",
13 | "between-interactions": "$1 sa pagitan ng mga interaksyon",
14 | "error": "Error",
15 | "error-help": "$1, tapos $2.",
16 | "error-help-report": "Iulat ang error",
17 | "error-help-refresh": "i-refresh ang pahina",
18 | "error-message-request-url": "URL ng hiling",
19 | "error-suppressed-diff": "Hindi mo makikita ang pagkakaibang ito dahil tinago ang isa o parehong pagbabago.",
20 | "info-no-users": "Magbigay ng dalawang tagagamit para magsimula.",
21 | "info-no-wiki": "Magbigay ng wiki para magsimula.",
22 | "info-required-fields": "Magbigay ng dalawang tagagamit at wiki para magsimula.",
23 | "licensed-under": "Lisensiyado sa ilalim ng $1",
24 | "field-label-end-date": "Dulo",
25 | "field-label-start-date": "Simula",
26 | "field-label-users": "Mga tagagamit",
27 | "field-label-wiki": "Wiki",
28 | "field-select-placeholder": "Pumili...",
29 | "field-select-no-results": "Walang nakitang resulta",
30 | "field-select-search-prompt": "Mag-type para maghanap",
31 | "made-by": "Ginawa ng $1 ng Wikimedia Foundation.",
32 | "made-by-team": "Anti-Harassment Tools team",
33 | "privacy-policy": "Patakaran sa Pagkapribado",
34 | "powered-by": "Pinapagana ng Wikimedia Toolforge",
35 | "report-bug": "Mag-ulat ng bug",
36 | "revision-edit-summary-removed": "tinanggal ang buod",
37 | "view-source-code-on": "tingnan sa $1",
38 | "warning-no-results": "Walang resulta",
39 | "discuss-on-wiki": "Ibahagi ang mga resultang ito",
40 | "discuss-on-wiki-text": "Mula $1 hanggang $2 sa $3, parehong gumawa ng mga edit sina [[User:$4]] ([[User talk:$4|usapan]] | [[Special:Contributions/$4|ambag]]) at [[User:$5]] ([[User talk:$5|usapan]] | [[Special:Contributions/$5|ambag]]) sa ilang mga pahina. Makikita mo ang isang kronolohikal na listahan ng lahat ng mga edit nila sa [$6 Interaction Timeline] o sa table na [$7 Interaction Analyzer].",
41 | "discuss-on-wiki-help": "Kopyahin at i-paste ang wikitext na ito sa isang on-wiki na usapan para maibahagi ang mga resulta ng Interaction Timeline sa iba",
42 | "discuss-on-wiki-copy": "Kopyahin",
43 | "discuss-on-wiki-copied": "Nakopya na!"
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/components/fields/date-range.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import moment from 'moment';
4 | import { Message } from '@wikimedia/react.i18n';
5 | import DatePicker from './date-picker';
6 |
7 | class DateRange extends React.Component {
8 |
9 | constructor( props ) {
10 | super( props );
11 |
12 | // Determine the end of day.
13 | this.eod = moment().utc().endOf( 'day' );
14 |
15 | // Binding
16 | this.isValidDate = this.isValidDate.bind( this );
17 | this.isValidStartDate = this.isValidStartDate.bind( this );
18 | this.isValidEndDate = this.isValidEndDate.bind( this );
19 | }
20 |
21 | isValidDate( date ) {
22 | return date.diff( this.eod ) < 0;
23 | }
24 |
25 | isValidStartDate( date ) {
26 | if ( !this.props.endDate ) {
27 | return this.isValidDate( date );
28 | }
29 |
30 | if ( date.diff( this.props.endDate ) > 0 ) {
31 | return false;
32 | }
33 |
34 | return this.isValidDate( date );
35 | }
36 |
37 | isValidEndDate( date ) {
38 | if ( !this.props.startDate ) {
39 | return this.isValidDate( date );
40 | }
41 |
42 | if ( date.diff( this.props.startDate ) < 0 ) {
43 | return false;
44 | }
45 |
46 | return this.isValidDate( date );
47 | }
48 |
49 | render() {
50 | return (
51 |
52 |
53 |
54 |
60 |
61 |
62 |
63 |
69 |
70 |
71 | );
72 | }
73 | }
74 |
75 | DateRange.propTypes = {
76 | startDate: PropTypes.instanceOf( moment ),
77 | endDate: PropTypes.instanceOf( moment ),
78 | onStartDateChange: PropTypes.func.isRequired,
79 | onEndDateChange: PropTypes.func.isRequired
80 | };
81 |
82 | DateRange.defaultProps = {
83 | startDate: undefined,
84 | endDate: undefined
85 | };
86 |
87 | export default DateRange;
88 |
--------------------------------------------------------------------------------
/client/src/components/timeline/timeline.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import UserListContainer from './user-list.container';
4 | import DateRevisionsContainer from './date-revisions.container';
5 | import StatusContainer from './status.container';
6 | import BackToTopButton from './back-top';
7 |
8 | const Users = () => {
9 | let stickyHeader = React.createRef();
10 | return (
11 |
12 |
13 |
31 |
32 | );
33 | };
34 |
35 | const Status = ( { empty } ) => {
36 | let className = [
37 | 'status',
38 | 'row'
39 | ];
40 |
41 | if ( !empty ) {
42 | className = [
43 | ...className,
44 | 'has-content'
45 | ];
46 | }
47 |
48 | return (
49 |
58 | );
59 | };
60 |
61 | Status.propTypes = {
62 | empty: PropTypes.bool.isRequired
63 | };
64 |
65 | const Timeline = ( { status, empty } ) => {
66 | if ( status === 'notready' ) {
67 | return (
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | if ( empty ) {
75 | return (
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | return (
84 |
85 |
86 |
87 |
88 |
89 | );
90 | };
91 |
92 | Timeline.propTypes = {
93 | status: PropTypes.oneOf( [ 'notready', 'ready', 'fetching', 'done', 'error' ] ).isRequired,
94 | empty: PropTypes.bool.isRequired
95 | };
96 |
97 | export default Timeline;
98 |
--------------------------------------------------------------------------------
/i18n/sk.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Jaroslav.micek",
5 | "Yardom78"
6 | ]
7 | },
8 | "app-description": "Chronologická história dvoch používateľov na stránkach, na ktorých obaja urobili úpravy.",
9 | "app-feedback-link": "Pomoc",
10 | "app-title": "Interaction Timeline (Časová os interakcie)",
11 | "back-top": "Späť na začiatok",
12 | "between-edits": "$1 medzi úpravami",
13 | "between-interactions": "$1 medzi interakciami",
14 | "error": "Chyba",
15 | "error-help": "$1, a potom $2.",
16 | "error-help-report": "Nahlásiť chybu",
17 | "error-help-refresh": "obnoviť stránku",
18 | "error-message-request-url": "Požiadavka URL",
19 | "error-suppressed-diff": "Nemôžete vidieť tento rozdiel pretože jedna alebo obe zmeny boli zakázané.",
20 | "info-no-users": "Na začatie prosím uveďte dvoch používateľov.",
21 | "info-no-wiki": "Na začatie prosím uveďte wiki.",
22 | "info-required-fields": "Na začatie prosím uveďte dvoch používateľov a wiki.",
23 | "licensed-under": "Licencované cez $1",
24 | "field-label-end-date": "Dátum ukončenia",
25 | "field-label-start-date": "Dátum začiatku",
26 | "field-label-users": "Používatelia",
27 | "field-label-wiki": "Wiki",
28 | "field-select-placeholder": "Vybrať...",
29 | "field-select-no-results": "Neboli nájdené žiadne výsledky",
30 | "field-select-search-prompt": "Písať pre vyhľadanie",
31 | "made-by": "Vytvoril Wikimedia Foundation's $1.",
32 | "made-by-team": "Tým nástrojov proti obťažovaniu",
33 | "privacy-policy": "Ochrana osobných údajov",
34 | "powered-by": "Poháňané cez Wikimedia Toolforge",
35 | "report-bug": "Nahlásiť chybu",
36 | "revision-edit-summary-removed": "zhrnutie úprav odstránené",
37 | "view-source-code-on": "Zobraziť na $1",
38 | "warning-no-results": "Žiadne výsledky",
39 | "discuss-on-wiki": "Zdieľať tieto výsledky",
40 | "discuss-on-wiki-text": "Medzi $1 a $2 na $3 [[User:$4]] ([[User talk:$4|dialóg]] |[[Special:Contributions/$4|príspevky]]) a [[User:$5]] ([[User talk:$5|dialóg]] |[[Special:Contributions/$5|príspevky]]) obaja používatelia urobili nejaké úpravy na stránka v common. Na stránkach [$6 Interaction Timeline] môžete vidieť kompletný chronologický zoznam ich úprav alebo tabuľku na [$7 Interaction Analyser].",
41 | "discuss-on-wiki-help": "Kopírovať a prilepiť tento text na wiki diskusiu pre zdieľanie tejto časovej osi interakcie s ostatnými používateľmi",
42 | "discuss-on-wiki-copy": "Kopírovať",
43 | "discuss-on-wiki-copied": "Skopírované!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/pl.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Chrumps",
5 | "DeRudySoulStorm",
6 | "Rainbow P",
7 | "Tar Lócesilion",
8 | "WaldiSt",
9 | "Woytecr"
10 | ]
11 | },
12 | "app-description": "Chronologiczna historia edycji dwóch użytkowników edytujących te same strony.",
13 | "app-feedback-link": "Pomoc",
14 | "app-title": "Interaction Timeline",
15 | "back-top": "Powrót do góry",
16 | "between-edits": "$1 między edycjami",
17 | "between-interactions": "$1 pomiędzy interakcjami",
18 | "error": "Błąd",
19 | "error-help": "$1, a następnie $2.",
20 | "error-help-report": "Zgłoś błąd",
21 | "error-help-refresh": "odśwież stronę",
22 | "error-message-request-url": "URL zapytania:",
23 | "error-suppressed-diff": "Nie można wyświetlić tego porównania, ponieważ jedna lub obie wersje zostały wyłączone.",
24 | "info-no-users": "Wskaż dwóch użytkowników aby zacząć.",
25 | "info-no-wiki": "Podaj wiki, aby zacząć.",
26 | "info-required-fields": "Wskaż dwóch użytkowników i wiki, aby zacząć.",
27 | "licensed-under": "Licencjonowane na mocy $1",
28 | "field-label-end-date": "Data zakończenia",
29 | "field-label-start-date": "Data rozpoczęcia",
30 | "field-label-users": "Użytkownicy",
31 | "field-label-wiki": "Wiki",
32 | "field-select-placeholder": "Wybierz...",
33 | "field-select-no-results": "Nie znaleziono wyników",
34 | "field-select-search-prompt": "Wpisz, aby wyszukać",
35 | "made-by": "Wykonane przez $1 w Wikimedia Foundation.",
36 | "made-by-team": "Zespół ds. Zwalczania nękania",
37 | "privacy-policy": "Polityka prywatności",
38 | "powered-by": "Na platformie Wikimedia Toolforge",
39 | "report-bug": "Zgłoś błąd",
40 | "revision-edit-summary-removed": "(usunięto opis zmian)",
41 | "view-source-code-on": "Wyświetl na $1",
42 | "warning-no-results": "Brak wyników",
43 | "discuss-on-wiki": "Udostępnij te wyniki",
44 | "discuss-on-wiki-text": "Pomiędzy $1 i $2 na $3 [[User:$4]] ([[User talk:$4|dyskusja]] | [[Special:Contributions/$4|wkład]]) i [[User:$5]] ([[User talk:$5|dyskusja]] | [[Special:Contributions/$5|wkład]]) obydwaj zrobili zmiany na tych samych stronach. Możesz zobaczyć chronologiczną list wszystkich ich zmian na [$6 Osi Czasu Interakcji], albo w widoku tabeli w [$7 Analizatorze Interakcji].",
45 | "discuss-on-wiki-help": "Skopiuj i wklej ten wikitext do dyskusji na wiki, aby udostępnić wyniki Osi czasu interakcji innym osobom",
46 | "discuss-on-wiki-copy": "Kopiuj",
47 | "discuss-on-wiki-copied": "Skopiowano!"
48 | }
49 |
--------------------------------------------------------------------------------
/i18n/mk.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Bjankuloski06",
5 | "Vlad5250"
6 | ]
7 | },
8 | "app-description": "Времеследна историја за двајца корисници на страниците кајшто обајцата вршеле уредувања.",
9 | "app-feedback-link": "Помош",
10 | "app-title": "Заемодејствен времеслед",
11 | "back-top": "Најгоре",
12 | "between-edits": "$1 помеѓу уредувањата",
13 | "between-interactions": "$1 помеѓу заемодејствата",
14 | "error": "Грешка",
15 | "error-help": "$1, а потоа $2.",
16 | "error-help-report": "Пријави ја грешката",
17 | "error-help-refresh": "превчитај ја страницата",
18 | "error-message-request-url": "URL на барањето:",
19 | "error-suppressed-diff": "Не можете да ја гледате оваа разлика бидејќи една или обете преработки се притаени.",
20 | "info-no-users": "Укажете ги двајцата корисници за да почнете.",
21 | "info-no-wiki": "Укажете вики за да почнете.",
22 | "info-required-fields": "Укажете двајца корисници и вики за да почнете.",
23 | "licensed-under": "Под лиценцата $1",
24 | "field-label-end-date": "Завршен датум",
25 | "field-label-start-date": "Почетен датум",
26 | "field-label-users": "Корисници",
27 | "field-label-wiki": "Вики",
28 | "field-select-placeholder": "Изберете...",
29 | "field-select-no-results": "Не пронајдов ништо",
30 | "field-select-search-prompt": "Внесете за да пребарате",
31 | "made-by": "Изработено од $1 на Фондацијата Викимедија.",
32 | "made-by-team": "Екипата за алатките против малтретирање",
33 | "privacy-policy": "Заштита на личните податоци",
34 | "powered-by": "Овозможено од Викимедиините лаборатории за алатки",
35 | "report-bug": "Пријави грешка",
36 | "revision-edit-summary-removed": "отстранет описот на уредувањето",
37 | "view-source-code-on": "Преглед на $1",
38 | "warning-no-results": "Не најдов ништо",
39 | "discuss-on-wiki": "Сподели го исходов",
40 | "discuss-on-wiki-text": "Помеѓу $1 и $2 на $3 [[User:$4]] ([[User talk:$4|разговор]] | [[Special:Contributions/$4|придонеси]]) и [[User:$5]] ([[User talk:$5|разговор]] | [[Special:Contributions/$5|придонеси]]) направија уредувања на едни исти страници. Можете да го погледате времеследниот список на сите нивни уредувања на [$6 Заемодејствениот времеслед] или пак како табла на [$7 Анализаторот на заемодејства].",
41 | "discuss-on-wiki-help": "Прекопирајте го овој викитекст во расправа на вики за да го споделите овој исход на заемодејствениот времеслед и други",
42 | "discuss-on-wiki-copy": "Копирај",
43 | "discuss-on-wiki-copied": "Ископирано!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/gl.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Banjo",
5 | "Toliño"
6 | ]
7 | },
8 | "app-description": "Historial cronolóxico de dous usuarios en páxinas nas que ambos fixeron edicións.",
9 | "app-feedback-link": "Axuda",
10 | "app-title": "Cronoloxía de interaccións",
11 | "back-top": "Volver ao inicio",
12 | "between-edits": "$1 entre edicións",
13 | "between-interactions": "$1 entre interaccións",
14 | "error": "Erro",
15 | "error-help": "$1, e despois $2.",
16 | "error-help-report": "Informar do erro",
17 | "error-help-refresh": "actualizar a páxina",
18 | "error-message-request-url": "URL da solicitude",
19 | "error-suppressed-diff": "Non podes ver esta diferenza porque unha ou ámbalas dúas revisións foron suprimidas.",
20 | "info-no-users": "Proporciona dous usuarios para comezar.",
21 | "info-no-wiki": "Proporciona un wiki para comezar.",
22 | "info-required-fields": "Proporciona dous usuarios e un wiki para comezar.",
23 | "licensed-under": "Baixo a licenza $1",
24 | "field-label-end-date": "Data de fin",
25 | "field-label-start-date": "Data de inicio",
26 | "field-label-users": "Usuarios",
27 | "field-label-wiki": "Wiki",
28 | "field-select-placeholder": "Seleccionar...",
29 | "field-select-no-results": "Non se atopou ningún resultado",
30 | "field-select-search-prompt": "Escribe para procurar",
31 | "made-by": "Feito polo $1 da Fundación Wikimedia.",
32 | "made-by-team": "equipo de ferramentas contra o acoso",
33 | "privacy-policy": "Política de protección de datos",
34 | "powered-by": "Impulsado por Wikimedia Toolforge",
35 | "report-bug": "Informar dun erro",
36 | "revision-edit-summary-removed": "resumo de edición eliminado",
37 | "view-source-code-on": "Ver en $1",
38 | "warning-no-results": "Sen resultados",
39 | "discuss-on-wiki": "Compartir estes resultados",
40 | "discuss-on-wiki-text": "Entre $1 e $2 en $3, os usuarios [[User:$4|$4]] ([[User talk:$4|conversa]] | [[Special:Contributions/$4|contribucións]]) e [[User:$5|$5]] ([[User talk:$5|conversa]] | [[Special:Contributions/$5|contribucións]]) fixeron edicións nalgunhas páxinas en común. Podes ver unha lista cronolóxica de todas as súas edicións na [$6 cronoloxía de interaccións] ou unha vista de táboa no [$7 analizador de interaccións].",
41 | "discuss-on-wiki-help": "Copia e pega este texto wiki nunha conversa do wiki para compartir estes resultados da cronoloxía de interaccións con outras persoas",
42 | "discuss-on-wiki-copy": "Copiar",
43 | "discuss-on-wiki-copied": "Copiado!"
44 | }
45 |
--------------------------------------------------------------------------------
/i18n/id.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Akmaie Ajam",
5 | "Penyuwangi",
6 | "Veracious"
7 | ]
8 | },
9 | "app-description": "Sejarah secara kronologis untuk dua pengguna di halaman dimana kedua-duanya melakukan suntingan.",
10 | "app-feedback-link": "Bantuan",
11 | "app-title": "Garis Waktu Interaksi",
12 | "back-top": "Kembali ke Atas",
13 | "between-edits": "$1 antara suntingan",
14 | "between-interactions": "$1 antara interaksi",
15 | "error": "Galat",
16 | "error-help": "$1, dan kemudian $2.",
17 | "error-help-report": "Laporkan kesalahan",
18 | "error-help-refresh": "muat ulang halaman",
19 | "error-message-request-url": "Minta URL",
20 | "error-suppressed-diff": "Anda tidak bisa melihat perbedaan ini karena salah satu atau kedua revisi telah disembunyikan.",
21 | "info-no-users": "Harap berikan dua pengguna untuk memulai.",
22 | "info-no-wiki": "Harap berikan wiki untuk memulai.",
23 | "info-required-fields": "Harap berikan dua pengguna dan wiki untuk memulai.",
24 | "licensed-under": "Dilisensikan di bawah $1",
25 | "field-label-end-date": "Tanggal Selesai",
26 | "field-label-start-date": "Tanggal Dimulai",
27 | "field-label-users": "Pengguna",
28 | "field-label-wiki": "Wiki",
29 | "field-select-placeholder": "Pilih...",
30 | "field-select-no-results": "Tidak ada hasil yang ditemukan",
31 | "field-select-search-prompt": "Ketik untuk mencari",
32 | "made-by": "Dibuat oleh $1 Wikimedia Foundation.",
33 | "made-by-team": "Tim Alat Anti-Gangguan",
34 | "privacy-policy": "Kebijakan privasi",
35 | "powered-by": "Didukung oleh Wikimedia Toolforge",
36 | "report-bug": "Laporkan bug/kutu",
37 | "revision-edit-summary-removed": "ringkasan suntingan dihapus",
38 | "view-source-code-on": "Lihat di $1",
39 | "warning-no-results": "Tidak Ada Hasil",
40 | "discuss-on-wiki": "Bagikan hasil tersebut",
41 | "discuss-on-wiki-text": "Antara $1 dan $2 pada $3 [[Pengguna:$4]] ([[Pembicaraan Pengguna:$4|bicara]] | [[Istimewa:Kontribusi/$4|kontribusi]]) dan [[Pengguna:$5]] ([[Pembicaraan Pengguna:$5|bicara]] | [[Istimewa:Kontribusi/$5|contribs]]) keduanya menyunting halaman tertentu. Anda dapat melihat daftar kronologis semua penyuntingan mereka pada [$6 Garis Waktu Interaksi] atau daftar tabel pada or [$7 Garis Waktu Interaksi].",
42 | "discuss-on-wiki-help": "Salin & tempel teks wiki ini ke diskusi pada wiki untuk membagikan hasil Linimasa Interaksi dengan orang lain",
43 | "discuss-on-wiki-copy": "Salin",
44 | "discuss-on-wiki-copied": "Berhasil disalin!"
45 | }
46 |
--------------------------------------------------------------------------------
/i18n/nl.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Klaas van Buiten",
5 | "KlaasZ4usV",
6 | "Optilete",
7 | "Siebrand"
8 | ]
9 | },
10 | "app-description": "Chronologische geschiedenis voor twee gebruikers op pagina's waarop ze allebei wijzigingen hebben aangebracht.",
11 | "app-feedback-link": "Hulp",
12 | "app-title": "Interactietijdlijn",
13 | "back-top": "Terug naar boven",
14 | "between-edits": "$1 tussen bewerkingen",
15 | "between-interactions": "$1 tussen interacties",
16 | "error": "Fout",
17 | "error-help": "$1, en dan $2.",
18 | "error-help-report": "Fout melden",
19 | "error-help-refresh": "pagina verversen",
20 | "error-message-request-url": "Verzoek-URL:",
21 | "error-suppressed-diff": "U kunt dit verschil niet bekijken omdat een of beide versies zijn onderdrukt.",
22 | "info-no-users": "Geef twee gebruikersnamen op om te beginnen.",
23 | "info-no-wiki": "Geef een wiki op om te beginnen.",
24 | "info-required-fields": "Geef twee gebruikersnamen en een wiki op om te beginnen.",
25 | "licensed-under": "Licentie: $1",
26 | "field-label-end-date": "Einddatum",
27 | "field-label-start-date": "Begindatum",
28 | "field-label-users": "Gebruikers",
29 | "field-label-wiki": "Wiki",
30 | "field-select-placeholder": "Selecteer...",
31 | "field-select-no-results": "Geen resultaten gevonden",
32 | "field-select-search-prompt": "Typ om te zoeken",
33 | "made-by": "Gemaakt door $1 van de Wikimedia Foundation.",
34 | "made-by-team": "Anti-Harassment Tools-team",
35 | "privacy-policy": "Privacybeleid",
36 | "powered-by": "Mogelijk gemaakt door Wikimedia Toolforge",
37 | "report-bug": "Probleem melden",
38 | "revision-edit-summary-removed": "bewerkingssamenvatting verwijderd",
39 | "view-source-code-on": "Bekijken op $1",
40 | "warning-no-results": "Geen resultaten",
41 | "discuss-on-wiki": "Resultaten delen",
42 | "discuss-on-wiki-text": "Tussen $1 en $2 op $3 [[User:$4]] ([[User talk:$4|overleg]] | [[Special:Contributions/$4|bijdragen]]) en [[User:$5]] ([[User talk:$5|overleg]] | [[Special:Contributions/$5|bijdragen]]) hebben beide wijzigingen aangebracht op enkele gemeenschappelijke pagina's. U kunt een chronologische lijst van al hun bewerkingen bekijken op de [$6 Interactietijdlijn] of een tabelweergave op de [$7 Interactieanalyse].",
43 | "discuss-on-wiki-help": "Kopieer en plak deze wikitekst in een overleg op de wiki om deze interactietijdlijnresultaten met anderen te delen",
44 | "discuss-on-wiki-copy": "Kopiëren",
45 | "discuss-on-wiki-copied": "Gekopieerd!"
46 | }
47 |
--------------------------------------------------------------------------------
/i18n/sc.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "L2212"
5 | ]
6 | },
7 | "app-description": "Istòria cronològica pro duos impitadores chi ant modificadu sas matessi pàginas.",
8 | "app-feedback-link": "Agiudu",
9 | "app-title": "Cronologia de sas interatziones",
10 | "back-top": "Torra a su cumintzu",
11 | "between-edits": "$1 intre modìficas",
12 | "between-interactions": "$1 intre interatziones",
13 | "error": "Faddina",
14 | "error-help": "$1, e a pustis $2.",
15 | "error-help-report": "Sinnala sa faddina",
16 | "error-help-refresh": "annoa sa pàgina",
17 | "error-message-request-url": "URL de sa dimanda",
18 | "error-suppressed-diff": "Non podes bìdere custa diferèntzia ant suprimidu una o ambas duas revisiones.",
19 | "info-no-users": "Dislinda duos impitadores pro incumintzare.",
20 | "info-no-wiki": "Dislinda una wiki pro incumintzare.",
21 | "info-required-fields": "Dislinda duos impitadores e una wiki pro incumintzare.",
22 | "licensed-under": "In suta de sa litzèntzia $1",
23 | "field-label-end-date": "Data Finale",
24 | "field-label-start-date": "Data de incumintzu",
25 | "field-label-users": "Impitadores",
26 | "field-label-wiki": "Wiki",
27 | "field-select-placeholder": "Ischerta...",
28 | "field-select-no-results": "Perunu resurtadu agatadu",
29 | "field-select-search-prompt": "Iscrie pro chircare",
30 | "made-by": "Fata dae sa $1 de sa Fundatzione Wikimedia.",
31 | "made-by-team": "Iscuadra de ainas anti-apurias",
32 | "privacy-policy": "Polìtica pro sos datos privados",
33 | "powered-by": "Funtzionat cun Wikimedia Toolforge",
34 | "report-bug": "Sinnala una faddina",
35 | "revision-edit-summary-removed": "resumu de sas modìficas bogadu",
36 | "view-source-code-on": "Ammustra in $1",
37 | "warning-no-results": "Perunu resurtadu",
38 | "discuss-on-wiki": "Cumpartzi custos resurtados",
39 | "discuss-on-wiki-text": "Intre $1 e $2 in $3 [[User:$4]] ([[User talk:$4|talk]] | [[Special:Contributions/$4|contribs]]) e [[User:$5]] ([[User talk:$5|talk]] | [[Special:Contributions/$5|contribs]]) ant totu sos duos fatu modìficas a unas cantas matessi pàginas. Podes bìdere sa lista cronològica de totu sas modìficas issoro in sa [$6 Cronologia de sas interatziones] o una vista a tabella in s'[$7 Analizadore de interatziones].",
40 | "discuss-on-wiki-help": "Còpia e incolla custu wikitestu in un'arresonu in sa wiki pro cumpartzire custos resurtados de sa cronologia de sas interatziones cun sos àteros",
41 | "discuss-on-wiki-copy": "Còpia",
42 | "discuss-on-wiki-copied": "Copiadu!"
43 | }
44 |
--------------------------------------------------------------------------------
/i18n/pt-br.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Chicocvenancio",
5 | "Eduardo Addad de Oliveira",
6 | "Eduardoaddad",
7 | "Fitoschido"
8 | ]
9 | },
10 | "app-description": "Histórico cronológico para dois usuários nas páginas em que ambos fizeram edições.",
11 | "app-feedback-link": "Ajuda",
12 | "app-title": "Linha do tempo de interação",
13 | "back-top": "Voltar ao topo",
14 | "between-edits": "$1 entre edições",
15 | "between-interactions": "$1 entre interações",
16 | "error": "Erro",
17 | "error-help": "$1, e depois $2",
18 | "error-help-report": "Reporte o erro",
19 | "error-help-refresh": "recarregue a página",
20 | "error-message-request-url": "URL solicitante:",
21 | "error-suppressed-diff": "Não podes ver esse diferencial porque um ou ambas as revisões foram suprimidas.",
22 | "info-no-users": "Por favor escolha dois usuários para começar.",
23 | "info-no-wiki": "Por favor escolha uma wiki para começar.",
24 | "info-required-fields": "Por favor escolha dois usuários e uma wiki para começar",
25 | "licensed-under": "Licenciado sob $1.",
26 | "field-label-end-date": "Data final",
27 | "field-label-start-date": "Data inicial",
28 | "field-label-users": "Usuários",
29 | "field-label-wiki": "Wiki",
30 | "field-select-placeholder": "Selecione...",
31 | "field-select-no-results": "Nenhum resultado encontrado",
32 | "field-select-search-prompt": "Escreva para buscar",
33 | "made-by": "Feito pela equipe $1 da Fundação Wikimedia.",
34 | "made-by-team": "Equipe de ferramentas anti-assédio",
35 | "privacy-policy": "Politica de privacidade",
36 | "powered-by": "Com a tecnologia Wikimedia Toolforge",
37 | "report-bug": "Reportar um erro",
38 | "revision-edit-summary-removed": "resumo da edição suprimido",
39 | "view-source-code-on": "Ver em $1",
40 | "warning-no-results": "Sem resultados",
41 | "discuss-on-wiki": "Compartilhar estes resultados",
42 | "discuss-on-wiki-text": "$1 e $2 em $3 [[User:$4]] ([[User talk:$4|disc]] | [[Special:Contributions/$4|contribs]]) e [[User:$5]] ([[User talk:$5|disc]] | [[Special:Contributions/$5|contribs]]) editaram ambos algumas páginas em comum. Pode ver uma lista cronológica de todas as edições de ambos na [$6 cronologia de interações] ou em forma de tabela no [$7 analisador de interações].",
43 | "discuss-on-wiki-help": "Copie e cole este wikitexto em uma discussão on-wiki para compartilhar esses resultados da linha do tempo de interação com outras pessoas",
44 | "discuss-on-wiki-copy": "Copiar",
45 | "discuss-on-wiki-copied": "Copiado!"
46 | }
47 |
--------------------------------------------------------------------------------
/i18n/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Amire80",
5 | "Happy13241",
6 | "Movses",
7 | "Okras",
8 | "Pacha Tchernof",
9 | "Patrick Star",
10 | "Vlad5250"
11 | ]
12 | },
13 | "app-description": "Хронологическая история для двух пользователей на страницах, где они оба вносили изменения.",
14 | "app-feedback-link": "Справка",
15 | "app-title": "Interaction Timeline",
16 | "back-top": "Вернуться наверх",
17 | "between-edits": "$1 между правками",
18 | "between-interactions": "$1 между взаимодействиями",
19 | "error": "Ошибка",
20 | "error-help": "$1, а затем $2.",
21 | "error-help-report": "Сообщить об ошибке",
22 | "error-help-refresh": "обновить страницу",
23 | "error-message-request-url": "URL-адрес запроса",
24 | "error-suppressed-diff": "Вы не можете просмотреть это разницу, поскольку одна или обе версии были скрыты.",
25 | "info-no-users": "Пожалуйста укажите двух участников.",
26 | "info-no-wiki": "Пожалуйста укажите вики для анализа.",
27 | "info-required-fields": "Пожалуйста укажите двух участников и вики.",
28 | "licensed-under": "Под лицензией $1",
29 | "field-label-end-date": "Дата окончания",
30 | "field-label-start-date": "Дата начала",
31 | "field-label-users": "Участники",
32 | "field-label-wiki": "Вики",
33 | "field-select-placeholder": "Выберите…",
34 | "field-select-no-results": "Ничего не найдено",
35 | "field-select-search-prompt": "Введите для поиска",
36 | "made-by": "Сделано командой $1 Фонда Викимедиа.",
37 | "made-by-team": "Команда по борьбе с преследованием",
38 | "privacy-policy": "Политика конфиденциальности",
39 | "powered-by": "На платформе Wikimedia Toolforge",
40 | "report-bug": "Сообщить об ошибке",
41 | "revision-edit-summary-removed": "описание правки удалено",
42 | "view-source-code-on": "Посмотреть на сайте $1",
43 | "warning-no-results": "Нет результатов",
44 | "discuss-on-wiki": "Поделиться этими результатами",
45 | "discuss-on-wiki-text": "Между $1 и $2 на вики $3 два участника [[User:$4]] ([[User talk:$4|обс.]] | [[Special:Contributions/$4|вклад]]) и [[User:$5]] ([[User talk:$5|обс.]] | [[Special:Contributions/$5|вклад]]) сделали правки на некоторых страницах. Вы можете посмотреть хронологический список всех правок участников на [$6 Interaction Timeline] или в табличном виде в [$7 Interaction Analyser].",
46 | "discuss-on-wiki-help": "Скопируйте и вставьте этот викитекст, чтобы поделиться с другими результатами Interaction Timeline",
47 | "discuss-on-wiki-copy": "Копировать",
48 | "discuss-on-wiki-copied": "Скопировано"
49 | }
50 |
--------------------------------------------------------------------------------
/i18n/vi.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Leducthn",
5 | "Minh Nguyen",
6 | "Tuanminh01"
7 | ]
8 | },
9 | "app-description": "Lịch sử sửa đổi theo thứ tự của hai người dùng sửa đổi cùng một trang.",
10 | "app-feedback-link": "Trợ giúp",
11 | "app-title": "Dòng thời gian tương tác",
12 | "back-top": "Trở lên đầu trang",
13 | "between-edits": "$1 giữa các lần sửa đổi",
14 | "between-interactions": "$1 giữa các lần tương tác",
15 | "error": "Lỗi",
16 | "error-help": "$1 và sau đó $2.",
17 | "error-help-report": "Báo cáo lỗi",
18 | "error-help-refresh": "tải lại trang",
19 | "error-message-request-url": "URL được yêu cầu",
20 | "error-suppressed-diff": "Bạn không thể xem sự khác biệt giữa cả hai vì một hoặc cả hai bản sửa đổi đã không còn nữa.",
21 | "info-no-users": "Xin hãy cung cấp hai người dùng để bắt đầu.",
22 | "info-no-wiki": "Xin hãy cung cấp một wiki để bắt đầu.",
23 | "info-required-fields": "Xin hãy cung cấp hai người dùng và một wiki để bắt đầu.",
24 | "licensed-under": "Phát hành theo giấy phép $1",
25 | "field-label-end-date": "Ngày kết thúc",
26 | "field-label-start-date": "Ngày bắt đầu",
27 | "field-label-users": "Người dùng",
28 | "field-label-wiki": "Wiki",
29 | "field-select-placeholder": "Chọn…",
30 | "field-select-no-results": "Không tìm thấy kết quả",
31 | "field-select-search-prompt": "Nhập vào để tìm",
32 | "made-by": "Được tạo ra bởi $1 của Quỹ Wikimedia.",
33 | "made-by-team": "Đội ngũ Công cụ Chống Quấy rối",
34 | "privacy-policy": "Chính sách riêng tư",
35 | "powered-by": "Chạy trên Phòng thí nghiệm Công cụ Wikimedia (Wikimedia Toolforge)",
36 | "report-bug": "Báo lỗi",
37 | "revision-edit-summary-removed": "tóm lược sửa đổi đã bị xóa",
38 | "view-source-code-on": "Xem tại $1",
39 | "warning-no-results": "Không có kết quả",
40 | "discuss-on-wiki": "Chia sẻ kết quả",
41 | "discuss-on-wiki-text": "Trong thời gian từ $1 tới $2 trên $3 ([[User talk:$4|thảo luận thành viên]] của [[User:$4]]| [[Special:Contributions/$4|đóng góp]]) và ([[User talk:$5|thảo luận thành viên]] của [[User:$5]]| [[Special:Contributions/$5|đóng góp]]) đều đã thực hiện thay đổi một số trang chung. Bạn có thể xem một danh sách các thay đổi này theo thời giantaiạ [$6 Dòng thời gian tương tác] hoặc theo bảng ở [$7 Phân tích tương tác].",
42 | "discuss-on-wiki-help": "Sao chép và dán wikitext này vào một cuộc thảo luận trên wiki để chia sẻ các kết quả Dòng thời gian tương tác này với những người khác",
43 | "discuss-on-wiki-copy": "Chép",
44 | "discuss-on-wiki-copied": "Đã sao chép!"
45 | }
46 |
--------------------------------------------------------------------------------
/i18n/pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "@metadata": {
3 | "authors": [
4 | "Athena in Wonderland",
5 | "Hamilton Abreu"
6 | ]
7 | },
8 | "app-description": "O historial cronológico de dois utilizadores nas páginas onde ambos realizaram edições.",
9 | "app-feedback-link": "Ajuda",
10 | "app-title": "Cronologia de interação",
11 | "back-top": "Voltar ao topo",
12 | "between-edits": "$1 entre edições",
13 | "between-interactions": "$1 entre interações",
14 | "error": "Erro",
15 | "error-help": "$1, e depois $2.",
16 | "error-help-report": "Comunicar o erro",
17 | "error-help-refresh": "atualizar a página",
18 | "error-message-request-url": "URL do pedido",
19 | "error-suppressed-diff": "Não pode ver estas diferenças entre revisões, porque uma ou ambas as revisões foram suprimidas.",
20 | "info-no-users": "Escolha dois utilizadores para começar, por favor.",
21 | "info-no-wiki": "Escolha uma wiki para começar, por favor.",
22 | "info-required-fields": "Escolha dois utilizadores e a wiki para começar, por favor.",
23 | "licensed-under": "Licenciado ao abrigo da licença $1",
24 | "field-label-end-date": "Data final",
25 | "field-label-start-date": "Data inicial",
26 | "field-label-users": "Utilizadores",
27 | "field-label-wiki": "Wiki",
28 | "field-select-placeholder": "Selecionar...",
29 | "field-select-no-results": "Não foram encontrados resultados",
30 | "field-select-search-prompt": "Escreva para procurar",
31 | "made-by": "Realizado por $1 da Fundação Wikimedia.",
32 | "made-by-team": "Equipa encarregada das ferramentas contra o assédio",
33 | "privacy-policy": "Política de privacidade",
34 | "powered-by": "Desenvolvido por Wikimedia Toolforge",
35 | "report-bug": "Reportar um defeito",
36 | "revision-edit-summary-removed": "resumo da edição suprimido",
37 | "view-source-code-on": "Ver em $1",
38 | "warning-no-results": "Sem resultados",
39 | "discuss-on-wiki": "Partilhar estes resultados",
40 | "discuss-on-wiki-text": "$1 e $2 em $3 [[User:$4]] ([[User talk:$4|disc]] | [[Special:Contributions/$4|contribs]]) e [[User:$5]] ([[User talk:$5|disc]] | [[Special:Contributions/$5|contribs]]) editaram ambos algumas páginas em comum. Pode ver uma lista cronológica de todas as edições de ambos na [$6 cronologia de interações] ou em forma de tabela no [$7 analisador de interações].",
41 | "discuss-on-wiki-help": "Copie e insira este texto wiki numa discussão na wiki, para partilhar estes resultados da cronologia de interações com outros utilizadores",
42 | "discuss-on-wiki-copy": "Copiar",
43 | "discuss-on-wiki-copied": "Copiado!"
44 | }
45 |
--------------------------------------------------------------------------------