git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Commit 05dc0664a7d196eeab3f1883b04f917b5b944013

Merge branch 'i18n-reload' of https://github.com/gmarcos87/patchwork into gmarcos87-i18n-reload

Matt McKegg committed on 9/30/2017, 2:44:36 AM
Parent: 0e7c0899affc876a69d822ae746c7c358f8c2906
Parent: db20e2ee1a31d6f09639c346e37894ebd770f6b3

Files changed

index.jschanged
lib/window.jschanged
main-window.jschanged
modules/app/html/progress-notifier.jschanged
modules/app/html/search.jschanged
modules/feed/html/rollup.jschanged
modules/gathering/sheet/edit.jschanged
modules/invite/sheet.jschanged
modules/message/async/publish.jschanged
modules/message/html/backlinks.jschanged
modules/message/html/compose.jschanged
modules/message/sheet/likes.jschanged
modules/page/html/render/all.jschanged
modules/page/html/render/channel.jschanged
modules/page/html/render/channels.jschanged
modules/page/html/render/gatherings.jschanged
modules/page/html/render/message.jschanged
modules/page/html/render/private.jschanged
modules/page/html/render/profile.jschanged
modules/page/html/render/public.jschanged
modules/page/html/render/search.jschanged
modules/page/html/render/settings.jschanged
modules/profile/sheet/edit.jschanged
overrides/patchcore/lib/timeAgo.jsadded
package.jsonchanged
plugs/message/html/layout/default.jschanged
plugs/message/html/meta/likes.jschanged
plugs/message/html/render/about.jschanged
plugs/message/html/render/channel.jschanged
plugs/message/html/render/following.jschanged
plugs/intl/sync/i18n.jsadded
locales/en.jsonadded
locales/es.jsonadded
locales/ki.jsonadded
index.jsView
@@ -18,8 +18,9 @@
1818 }
1919 var ssbConfig = null
2020 var quitting = false
2121
22+
2223 electron.app.on('ready', () => {
2324 setupContext('ssb', {
2425 server: !(process.argv.includes('-g') || process.argv.includes('--use-global-ssb'))
2526 }, () => {
@@ -79,9 +80,9 @@
7980 width: windowState.width,
8081 height: windowState.height,
8182 titleBarStyle: 'hidden-inset',
8283 autoHideMenuBar: true,
83- title: 'Patchwork',
84+ title: "Patchwork",
8485 show: true,
8586 backgroundColor: '#EEE',
8687 webPreferences: {
8788 experimentalFeatures: true
lib/window.jsView
@@ -18,9 +18,9 @@
1818 electron.webFrame.setZoomLevelLimits(1, 1)
1919
2020 var config = ${JSON.stringify(config)}
2121 var data = ${JSON.stringify(opts.data)}
22- var title = ${JSON.stringify(opts.title || 'Patchwork')}
22+ var title = ${JSON.stringify(opts.title || "Patchwork" )}
2323
2424 document.documentElement.querySelector('head').appendChild(
2525 h('title', title)
2626 )
main-window.jsView
@@ -12,8 +12,9 @@
1212 var ref = require('ssb-ref')
1313 var setupContextMenuAndSpellCheck = require('./lib/context-menu-and-spellcheck')
1414 var watch = require('mutant/watch')
1515
16+
1617 module.exports = function (config) {
1718 var sockets = combine(
1819 overrideConfig(config),
1920 addCommand('app.navigate', setView),
@@ -39,16 +40,19 @@
3940 'profile.sheet.edit': 'first',
4041 'app.navigate': 'first',
4142 'channel.obs.subscribed': 'first',
4243 'settings.obs.get': 'first',
44+ 'intl.sync.i18n': 'first',
4345 }))
4446
4547 setupContextMenuAndSpellCheck(api.config.sync.load())
48+
49+ const i18n = api.intl.sync.i18n
4650
4751 var id = api.keys.sync.id()
4852 var latestUpdate = LatestUpdate()
4953 var subscribedChannels = api.channel.obs.subscribed(id)
50-
54+
5155 // prompt to setup profile on first use
5256 onceTrue(api.sbot.obs.connection, (sbot) => {
5357 sbot.latestSequence(sbot.id, (_, key) => {
5458 if (key == null) {
@@ -90,32 +94,32 @@
9094 classList: [ when(views.canGoForward, '-active') ]
9195 })
9296 ]),
9397 h('span.nav', [
94- tab('Public', '/public'),
95- tab('Private', '/private'),
96- dropTab('More', [
98+ tab(i18n("Public"), '/public'),
99+ tab(i18n("Private"), '/private'),
100+ dropTab(i18n('More'), [
97101 getSubscribedChannelMenu,
98- ['Gatherings', '/gatherings'],
99- ['Extended Network', '/all'],
102+ [i18n('Gatherings'), '/gatherings'],
103+ [i18n('Extended Network'), '/all'],
100104 {separator: true},
101- ['Settings', '/settings']
105+ [i18n('Settings'), '/settings']
102106 ])
103107 ]),
104108 h('span.appTitle', [
105- h('span.title', 'Patchwork'),
109+ h('span.title', i18n("Patchwork")),
106110 api.app.html.progressNotifier()
107111 ]),
108112 h('span', [ api.app.html.search(api.app.navigate) ]),
109113 h('span.nav', [
110- tab('Profile', id),
111- tab('Mentions', '/mentions')
114+ tab(i18n('Profile'), id),
115+ tab(i18n('Mentions'), '/mentions')
112116 ])
113117 ]),
114118 when(latestUpdate,
115119 h('div.info', [
116120 h('a.message -update', { href: 'https://github.com/ssbc/patchwork/releases' }, [
117- h('strong', ['Patchwork ', latestUpdate, ' has been released.']), ' Click here to download and view more info!',
121+ h('strong', ['Patchwork ', latestUpdate, i18n(' has been released.')]), i18n(' Click here to download and view more info!'),
118122 h('a.ignore', {'ev-click': latestUpdate.ignore}, 'X')
119123 ])
120124 ])
121125 ),
@@ -149,11 +153,11 @@
149153 var channels = Array.from(subscribedChannels()).sort(localeCompare)
150154
151155 if (channels.length) {
152156 return {
153- label: 'Channels',
157+ label: i18n('Channels'),
154158 submenu: [
155- { label: 'Browse All',
159+ { label: i18n('Browse All'),
156160 click () {
157161 setView('/channels')
158162 }
159163 },
@@ -168,9 +172,9 @@
168172 }))
169173 }
170174 } else {
171175 return {
172- label: 'Browse Channels',
176+ label: i18n('Browse Channels'),
173177 click () {
174178 setView('/channels')
175179 }
176180 }
modules/app/html/progress-notifier.jsView
@@ -1,8 +1,8 @@
11 var {computed, when, h, Value} = require('mutant')
22 var nest = require('depnest')
33 var sustained = require('../../../lib/sustained')
4-var pull = require('pull-stream')
4+const pull = require('pull-stream')
55
66 exports.gives = nest('app.html.progressNotifier')
77
88 exports.needs = nest({
@@ -11,12 +11,14 @@
1111 'progress.obs': {
1212 indexes: 'first',
1313 replicate: 'first',
1414 migration: 'first'
15- }
15+ },
16+ 'intl.sync.i18n':'first'
1617 })
1718
1819 exports.create = function (api) {
20+ const i18n = api.intl.sync.i18n
1921 return nest('app.html.progressNotifier', function (id) {
2022 var replicateProgress = api.progress.obs.replicate()
2123 var indexes = api.progress.obs.indexes()
2224 var migration = api.progress.obs.migration()
@@ -45,17 +47,15 @@
4547
4648 return h('div.info', { hidden }, [
4749 h('div.status', [
4850 when(displaying, h('Loading -small', [
49- when(waiting, 'Waiting for Scuttlebot...',
50- when(pendingMigration,
51- [h('span.info', 'Upgrading database'), h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: migrationProgress })],
52- when(computed(replicateProgress.incompleteFeeds, (v) => v > 5),
53- [h('span.info', 'Downloading new messages'), h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: downloadProgress })],
54- when(pending, [
55- [h('span.info', 'Indexing database'), h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: indexProgress })]
56- ], 'Scuttling...')
57- )
51+ when(pendingMigration,
52+ [h('span.info', i18n('Upgrading database')), h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: migrationProgress })],
53+ when(computed(replicateProgress.incompleteFeeds, (v) => v > 5),
54+ [h('span.info', i18n('Downloading new messages')), h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: downloadProgress })],
55+ when(pending, [
56+ [h('span.info', i18n('Indexing database')), h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: indexProgress })]
57+ ], i18n('Scuttling...'))
5858 )
5959 )
6060 ]))
6161 ])
modules/app/html/search.jsView
@@ -1,24 +1,28 @@
11 var h = require('mutant/h')
22 var nest = require('depnest')
33 var addSuggest = require('suggest-box')
44
5+var appRoot = require('app-root-path');
6+
57 exports.needs = nest({
68 'profile.async.suggest': 'first',
7- 'channel.async.suggest': 'first'
9+ 'channel.async.suggest': 'first',
10+ 'intl.sync.i18n': 'first'
811 })
912
1013 exports.gives = nest('app.html.search')
1114
1215 var pages = ['/public', '/private', '/mentions', '/all', '/gatherings']
1316
1417 exports.create = function (api) {
18+ const i18n = api.intl.sync.i18n
1519 return nest('app.html.search', function (setView) {
1620 var getProfileSuggestions = api.profile.async.suggest()
1721 var getChannelSuggestions = api.channel.async.suggest()
1822 var searchBox = h('input.search', {
1923 type: 'search',
20- placeholder: 'word, @key, #channel',
24+ placeholder: i18n('word, @key, #channel'),
2125 'ev-suggestselect': (ev) => {
2226 setView(ev.detail.id)
2327 searchBox.value = ev.detail.id
2428 },
modules/feed/html/rollup.jsView
@@ -17,8 +17,9 @@
1717
1818 // bump even for first message
1919 var rootBumpTypes = ['mention', 'channel-mention']
2020
21+
2122 exports.needs = nest({
2223 'about.obs.name': 'first',
2324 'app.sync.externalHandler': 'first',
2425 'message.html.render': 'first',
@@ -26,16 +27,18 @@
2627 'message.html.link': 'first',
2728 'message.sync.root': 'first',
2829 'feed.pull.rollup': 'first',
2930 'sbot.async.get': 'first',
30- 'keys.sync.id': 'first'
31+ 'keys.sync.id': 'first',
32+ 'intl.sync.i18n': 'first',
3133 })
3234
3335 exports.gives = nest({
3436 'feed.html.rollup': true
3537 })
3638
3739 exports.create = function (api) {
40+ const i18n = api.intl.sync.i18n
3841 return nest('feed.html.rollup', function (getStream, {
3942 prepend,
4043 rootFilter = returnTrue,
4144 bumpFilter = returnTrue,
@@ -46,9 +49,9 @@
4649 var updates = Value(0)
4750 var yourId = api.keys.sync.id()
4851 var throttledUpdates = throttle(updates, 200)
4952 var updateLoader = h('a Notifier -loader', { href: '#', 'ev-click': refresh }, [
50- 'Show ', h('strong', [throttledUpdates]), ' ', plural(throttledUpdates, 'update', 'updates')
53+ 'Show ', h('strong', [throttledUpdates]), ' ', plural(throttledUpdates, i18n('update'), i18n('updates'))
5154 ])
5255
5356 var abortLastFeed = null
5457 var content = Value()
@@ -190,11 +193,11 @@
190193 var bumps = lastBumpType === 'vote'
191194 ? getLikeAuthors(groupedBumps[lastBumpType])
192195 : getAuthors(groupedBumps[lastBumpType])
193196
194- var description = bumpMessages[lastBumpType] || 'added changes'
197+ var description = i18n(bumpMessages[lastBumpType] || 'added changes')
195198 meta = h('div.meta', { title: names(bumps) }, [
196- many(bumps, api.profile.html.person), ' ', description
199+ many(bumps, api.profile.html.person, i18n), ' ', description
197200 ])
198201 }
199202
200203 return h('FeedEvent -post', {
@@ -205,9 +208,9 @@
205208 meta,
206209 renderedMessage,
207210 when(replyElements.length, [
208211 when(replies.length > replyElements.length || partial,
209- h('a.full', {href: item.key}, ['View full thread (', replies.length, ')'])
212+ h('a.full', {href: item.key}, [i18n('View full thread') +' (', replies.length, ')'])
210213 ),
211214 h('div.replies', replyElements)
212215 ])
213216 ])
@@ -244,36 +247,36 @@
244247 }
245248 })
246249 }
247250
248-function many (ids, fn) {
251+function many (ids, fn, intl) {
249252 ids = Array.from(ids)
250253 var featuredIds = ids.slice(0, 4)
251254
252255 if (ids.length) {
253256 if (ids.length > 4) {
254257 return [
255258 fn(featuredIds[0]), ', ',
256259 fn(featuredIds[1]), ', ',
257- fn(featuredIds[2]), ' and ',
258- ids.length - 3, ' others'
260+ fn(featuredIds[2]), intl(' and '),
261+ ids.length - 3, intl(' others'),
259262 ]
260263 } else if (ids.length === 4) {
261264 return [
262265 fn(featuredIds[0]), ', ',
263266 fn(featuredIds[1]), ', ',
264- fn(featuredIds[2]), ' and ',
267+ fn(featuredIds[2]), intl(' and '),
265268 fn(featuredIds[3])
266269 ]
267270 } else if (ids.length === 3) {
268271 return [
269272 fn(featuredIds[0]), ', ',
270- fn(featuredIds[1]), ' and ',
273+ fn(featuredIds[1]), intl(' and '),
271274 fn(featuredIds[2])
272275 ]
273276 } else if (ids.length === 2) {
274277 return [
275- fn(featuredIds[0]), ' and ',
278+ fn(featuredIds[0]), intl(' and '),
276279 fn(featuredIds[1])
277280 ]
278281 } else {
279282 return fn(featuredIds[0])
modules/gathering/sheet/edit.jsView
@@ -12,12 +12,14 @@
1212 'keys.sync.id': 'first',
1313 'sbot.async.publish': 'first',
1414 'about.obs.latestValue': 'first',
1515 'blob.html.input': 'first',
16- 'blob.sync.url': 'first'
16+ 'blob.sync.url': 'first',
17+ 'intl.sync.i18n': 'first',
1718 })
1819
1920 exports.create = function (api) {
21+ const i18n = api.intl.sync.i18n
2022 return nest('gathering.sheet.edit', function (id) {
2123 api.sheet.display(close => {
2224 var current = id ? {
2325 title: api.about.obs.latestValue(id, 'title'),
@@ -52,44 +54,44 @@
5254 h('h2', {
5355 style: {
5456 'font-weight': 'normal'
5557 }
56- }, [id ? 'Edit' : 'Create', ' Gathering']),
58+ }, [id ? i18n('Edit') : i18n('Create'), i18n(' Gathering')]),
5759 h('GatheringEditor', [
5860 h('input.title', {
59- placeholder: 'Choose a title',
61+ placeholder: i18n('Choose a title'),
6062 hooks: [ValueHook(chosen.title), FocusHook()]
6163 }),
6264 h('input.date', {
63- placeholder: 'Choose date and time',
65+ placeholder: i18n('Choose date and time'),
6466 hooks: [
6567 PickrHook(chosen.startDateTime)
6668 ]
6769 }),
6870 h('ImageInput .banner', {
6971 style: { 'background-image': computed(imageUrl, x => `url(${x})`) }
7072 }, [
71- h('span', ['🖼 Choose Banner Image...']),
73+ h('span', ['🖼 ', i18n('Choose Banner Image...')]),
7274 api.blob.html.input(file => {
7375 chosen.image.set(file)
7476 }, {
7577 accept: 'image/*'
7678 })
7779 ]),
7880 h('textarea.description', {
79- placeholder: 'Describe the gathering (if you want)',
81+ placeholder: i18n('Describe the gathering (if you want)'),
8082 hooks: [ValueHook(chosen.description)]
8183 })
8284 ])
8385 ]),
8486 footer: [
8587 h('button -save', {
8688 'ev-click': save,
8789 'disabled': publishing
88- }, when(publishing, 'Publishing...', 'Publish')),
90+ }, when(publishing, i18n('Publishing...'), i18n('Publish'))),
8991 h('button -cancel', {
9092 'ev-click': close
91- }, 'Cancel')
93+ }, i18n('Cancel'))
9294 ]
9395 }
9496
9597 function ensureExists (cb) {
@@ -110,9 +112,9 @@
110112 var update = {}
111113
112114 if (!compareImage(chosen.image(), current.image())) update.image = chosen.image()
113115 if (!compareTime(chosen.startDateTime(), current.startDateTime())) update.startDateTime = chosen.startDateTime()
114- if (chosen.title() !== current.title()) update.title = chosen.title() || 'Untitled Gathering'
116+ if (chosen.title() !== current.title()) update.title = chosen.title() || i18n('Untitled Gathering')
115117 if (chosen.description() !== current.description()) update.description = chosen.description()
116118
117119 if (Object.keys(update).length) {
118120 publishing.set(true)
@@ -125,11 +127,11 @@
125127 if (err) {
126128 publishing.set(false)
127129 showDialog({
128130 type: 'error',
129- title: 'Error',
131+ title: i18n('Error'),
130132 buttons: ['OK'],
131- message: 'An error occurred while attempting to publish gathering.',
133+ message: i18n('An error occurred while attempting to publish gathering.'),
132134 detail: err.message
133135 })
134136 } else {
135137 close()
modules/invite/sheet.jsView
@@ -3,14 +3,16 @@
33 var electron = require('electron')
44
55 exports.needs = nest({
66 'sheet.display': 'first',
7- 'invite.async.accept': 'first'
7+ 'invite.async.accept': 'first',
8+ 'intl.sync.i18n': 'first',
89 })
910
1011 exports.gives = nest('invite.sheet')
1112
1213 exports.create = function (api) {
14+ const i18n = api.intl.sync.i18n
1315 return nest('invite.sheet', function () {
1416 api.sheet.display(close => {
1517 var publishing = Value()
1618 var publishStatus = Proxy()
@@ -20,9 +22,9 @@
2022 'font-size': '200%',
2123 'margin-top': '20px',
2224 'width': '100%'
2325 },
24- placeholder: 'paste invite code here'
26+ placeholder: i18n('paste invite code here')
2527 })
2628 setTimeout(() => {
2729 input.focus()
2830 input.select()
@@ -36,11 +38,11 @@
3638 h('h2', {
3739 style: {
3840 'font-weight': 'normal'
3941 }
40- }, ['By default, Patchwork will only see other users that are on the same local area network as you.']),
42+ }, [i18n('By default, Patchwork will only see other users that are on the same local area network as you.')]),
4143 h('div', [
42- 'In order to share with users on the internet, you need to be invited to a pub server.'
44+ i18n('In order to share with users on the internet, you need to be invited to a pub server.')
4345 ]),
4446 input
4547 ]),
4648 footer: [
@@ -52,22 +54,22 @@
5254 if (err) {
5355 publishing.set(false)
5456 showDialog({
5557 type: 'error',
56- title: 'Error',
57- buttons: ['OK'],
58- message: 'An error occurred while attempting to redeem invite.',
58+ title: i18n('Error'),
59+ buttons: [i18n('OK')],
60+ message: i18n('An error occurred while attempting to redeem invite.'),
5961 detail: err.message
6062 })
6163 } else {
6264 close()
6365 }
6466 }))
6567 }
66- }, [ when(publishing, publishStatus, 'Redeem Invite') ]),
68+ }, [ when(publishing, publishStatus, i18n('Redeem Invite')) ]),
6769 h('button -cancel', {
6870 'ev-click': close
69- }, 'Cancel')
71+ }, i18n('Cancel'))
7072 ]
7173 }
7274 })
7375 })
modules/message/async/publish.jsView
@@ -4,14 +4,16 @@
44 exports.needs = nest({
55 'sheet.display': 'first',
66 'message.html.render': 'first',
77 'sbot.async.publish': 'first',
8- 'keys.sync.id': 'first'
8+ 'keys.sync.id': 'first',
9+ 'intl.sync.i18n': 'first',
910 })
1011
1112 exports.gives = nest('message.async.publish')
1213
1314 exports.create = function (api) {
15+ const i18n = api.intl.sync.i18n
1416 return nest('message.async.publish', function (content, cb) {
1517 api.sheet.display(function (close) {
1618 return {
1719 content: [
@@ -21,10 +23,10 @@
2123 author: api.keys.sync.id()
2224 }})
2325 ],
2426 footer: [
25- h('button -save', { 'ev-click': publish }, 'Confirm'),
26- h('button -cancel', { 'ev-click': cancel }, 'Cancel')
27+ h('button -save', { 'ev-click': publish }, i18n('Confirm')),
28+ h('button -cancel', { 'ev-click': cancel }, i18n('Cancel'))
2729 ]
2830 }
2931
3032 function publish () {
modules/message/html/backlinks.jsView
@@ -7,14 +7,16 @@
77 backlinks: 'first',
88 name: 'first',
99 author: 'first'
1010 },
11- 'profile.html.person': 'first'
11+ 'profile.html.person': 'first',
12+ 'intl.sync.i18n': 'first',
1213 })
1314
1415 exports.gives = nest('message.html.backlinks')
1516
1617 exports.create = function (api) {
18+ const i18n = api.intl.sync.i18n
1719 return nest('message.html.backlinks', function (msg, {includeReferences = true, includeForks = true} = {}) {
1820 if (!ref.type(msg.key)) return []
1921 var backlinks = api.message.obs.backlinks(msg.key)
2022 var references = includeReferences ? computed([backlinks, msg], onlyReferences) : []
@@ -24,9 +26,9 @@
2426 return h('a.backlink', {
2527 href: link.id, title: link.id
2628 }, [
2729 h('strong', [
28- api.profile.html.person(link.author), ' forked this discussion:'
30+ api.profile.html.person(link.author), i18n(' forked this discussion:')
2931 ]), ' ',
3032 api.message.obs.name(link.id)
3133 ])
3234 }),
@@ -34,9 +36,9 @@
3436 return h('a.backlink', {
3537 href: link.id, title: link.id
3638 }, [
3739 h('strong', [
38- api.profile.html.person(link.author), ' referenced this message:'
40+ api.profile.html.person(link.author), i18n(' referenced this message:')
3941 ]), ' ',
4042 api.message.obs.name(link.id)
4143 ])
4244 })
modules/message/html/compose.jsView
@@ -14,14 +14,16 @@
1414 'profile.async.suggest': 'first',
1515 'channel.async.suggest': 'first',
1616 'message.async.publish': 'first',
1717 'emoji.sync.names': 'first',
18- 'emoji.sync.url': 'first'
18+ 'emoji.sync.url': 'first',
19+ 'intl.sync.i18n': 'first',
1920 })
2021
2122 exports.gives = nest('message.html.compose')
2223
2324 exports.create = function (api) {
25+ const i18n = api.intl.sync.i18n
2426 return nest('message.html.compose', function ({shrink = true, meta, prepublish, placeholder = 'Write a message'}, cb) {
2527 var files = []
2628 var filesById = {}
2729 var focused = Value(false)
@@ -95,9 +97,9 @@
9597
9698 var publishBtn = h('button', {
9799 'ev-click': publish,
98100 disabled: publishing
99- }, when(publishing, 'Publishing...', 'Publish'))
101+ }, when(publishing, i18n('Publishing...'), i18n('Publish')))
100102
101103 var actions = h('section.actions', [
102104 fileInput,
103105 publishBtn
modules/message/sheet/likes.jsView
@@ -8,22 +8,24 @@
88 'contact.obs.following': 'first',
99 'profile.obs.rank': 'first',
1010 'about.html.image': 'first',
1111 'about.obs.name': 'first',
12- 'app.navigate': 'first'
12+ 'app.navigate': 'first',
13+ 'intl.sync.i18n': 'first',
1314 })
1415
1516 exports.gives = nest('message.sheet.likes')
1617
1718 exports.create = function (api) {
19+ const i18n = api.intl.sync.i18n
1820 return nest('message.sheet.likes', function (ids) {
1921 api.sheet.display(close => {
2022 var content = h('div', {
2123 style: { padding: '20px' }
2224 }, [
2325 h('h2', {
2426 style: { 'font-weight': 'normal' }
25- }, ['Liked by']),
27+ }, [i18n('Liked by')]),
2628 renderContactBlock(ids)
2729 ])
2830
2931 catchLinks(content, (href, external) => {
@@ -37,9 +39,9 @@
3739 content,
3840 footer: [
3941 h('button -close', {
4042 'ev-click': close
41- }, 'Close')
43+ }, i18n('Close'))
4244 ]
4345 }
4446 })
4547 })
modules/page/html/render/all.jsView
@@ -4,29 +4,31 @@
44 exports.needs = nest({
55 'feed.pull.public': 'first',
66 'message.html.compose': 'first',
77 'message.async.publish': 'first',
8- 'feed.html.rollup': 'first'
8+ 'feed.html.rollup': 'first',
9+ 'intl.sync.i18n': 'first',
910 })
1011
1112 exports.gives = nest({
1213 'page.html.render': true
1314 })
1415
1516 exports.create = function (api) {
17+ const i18n = api.intl.sync.i18n
1618 return nest('page.html.render', page)
1719
1820 function page (path) {
1921 if (path !== '/all') return // "/" is a sigil for "page"
2022
2123 var prepend = [
2224 h('PageHeading', [
2325 h('h1', [
24- 'All Posts from Your ',
25- h('strong', 'Extended Network')
26+ i18n('All Posts from Your '),
27+ h('strong', i18n('Extended Network'))
2628 ])
2729 ]),
28- api.message.html.compose({ meta: { type: 'post' }, placeholder: 'Write a public message' })
30+ api.message.html.compose({ meta: { type: 'post' }, placeholder: i18n('Write a public message') })
2931 ]
3032
3133 var feedView = api.feed.html.rollup(api.feed.pull.public, {
3234 bumpFilter: (msg) => {
modules/page/html/render/channel.jsView
@@ -7,14 +7,16 @@
77 'feed.html.rollup': 'first',
88 'feed.pull.channel': 'first',
99 'sbot.pull.log': 'first',
1010 'message.async.publish': 'first',
11- 'keys.sync.id': 'first'
11+ 'keys.sync.id': 'first',
12+ 'intl.sync.i18n': 'first',
1213 })
1314
1415 exports.gives = nest('page.html.render')
1516
1617 exports.create = function (api) {
18+ const i18n = api.intl.sync.i18n
1719 return nest('page.html.render', function channel (path) {
1820 if (path[0] !== '#') return
1921
2022 var channel = path.substr(1)
@@ -26,21 +28,21 @@
2628 h('div.meta', [
2729 when(subscribedChannels.has(channel),
2830 h('a.ToggleButton.-unsubscribe', {
2931 'href': '#',
30- 'title': 'Click to unsubscribe',
32+ 'title': i18n('Click to unsubscribe'),
3133 'ev-click': send(unsubscribe, channel)
32- }, 'Subscribed'),
34+ }, i18n('Subscribed')),
3335 h('a.ToggleButton.-subscribe', {
3436 'href': '#',
3537 'ev-click': send(subscribe, channel)
36- }, 'Subscribe')
38+ }, i18n('Subscribe'))
3739 )
3840 ])
3941 ]),
4042 api.message.html.compose({
4143 meta: {type: 'post', channel},
42- placeholder: 'Write a message in this channel\n\n\n\nPeople who follow you or subscribe to this channel will also see this message in their main feed.\n\nTo create a new channel, type the channel name (preceded by a #) into the search box above. e.g #cat-pics'
44+ placeholder: i18n('Write a message in this channel')
4345 })
4446 ]
4547
4648 return api.feed.html.rollup(api.feed.pull.channel(channel), {
modules/page/html/render/channels.jsView
@@ -6,14 +6,16 @@
66 'keys.sync.id': 'first',
77 'channel.obs': {
88 subscribed: 'first',
99 recent: 'first'
10- }
10+ },
11+ 'intl.sync.i18n': 'first'
1112 })
1213
1314 exports.gives = nest('page.html.render')
1415
1516 exports.create = function(api){
17+ const i18n = api.intl.sync.i18n
1618 return nest('page.html.render', function page(path){
1719 if (path !== '/channels') return
1820
1921 var id = api.keys.sync.id()
@@ -38,12 +40,12 @@
3840 h('span.name', '#' + channel),
3941 when(subscribed,
4042 h('a.-unsubscribe', {
4143 'ev-click': send(unsubscribe, channel)
42- }, 'Unsubscribe'),
44+ }, i18n('Unsubscribe')),
4345 h('a.-subscribe', {
4446 'ev-click': send(subscribe, channel)
45- }, 'Subscribe')
47+ }, i18n('Subscribe'))
4648 )
4749 ])
4850 }, {maxTime: 5, idle: true})
4951 ])
modules/page/html/render/gatherings.jsView
@@ -7,27 +7,29 @@
77 'feed.pull.public': 'first',
88 'gathering.sheet.edit': 'first',
99 'keys.sync.id': 'first',
1010 'contact.obs.following': 'first',
11- 'sbot.pull.stream': 'first'
11+ 'sbot.pull.stream': 'first',
12+ 'intl.sync.i18n': 'first',
1213 })
1314
1415 exports.gives = nest('page.html.render')
1516
1617 exports.create = function (api) {
18+ const i18n = api.intl.sync.i18n
1719 return nest('page.html.render', function channel (path) {
1820 if (path !== '/gatherings') return
1921
2022 var id = api.keys.sync.id()
2123 var following = api.contact.obs.following(id)
2224
2325 var prepend = [
2426 h('PageHeading', [
25- h('h1', [h('strong', 'Gatherings'), ' from your extended network']),
27+ h('h1', [h('strong', i18n('Gatherings')), i18n(' from your extended network')]),
2628 h('div.meta', [
2729 h('button -add', {
2830 'ev-click': createGathering
29- }, '+ Add Gathering')
31+ }, i18n('+ Add Gathering'))
3032 ])
3133 ])
3234 ]
3335
modules/page/html/render/message.jsView
@@ -9,14 +9,16 @@
99 'message.html': {
1010 render: 'first',
1111 compose: 'first'
1212 },
13- 'sbot.async.get': 'first'
13+ 'sbot.async.get': 'first',
14+ 'intl.sync.i18n':'first',
1415 })
1516
1617 exports.gives = nest('page.html.render')
1718
1819 exports.create = function (api) {
20+ const i18n = api.intl.sync.i18n
1921 return nest('page.html.render', function (id) {
2022 if (!ref.isMsg(id)) return
2123 var loader = h('div', {className: 'Loading -large'})
2224
@@ -32,15 +34,15 @@
3234
3335 var compose = api.message.html.compose({
3436 meta,
3537 shrink: false,
36- placeholder: when(meta.recps, 'Write a private reply', 'Write a public reply')
38+ placeholder: when(meta.recps, i18n('Write a private reply'), i18n('Write a public reply'))
3739 })
3840
3941 api.sbot.async.get(id, (err, value) => {
4042 if (err) {
4143 return result.set(h('PageHeading', [
42- h('h1', 'Cannot load thread. Root message missing.')
44+ h('h1', i18n('Cannot load thead'))
4345 ]))
4446 }
4547
4648 if (typeof value.content === 'string') {
@@ -48,9 +50,9 @@
4850 }
4951
5052 if (!value) {
5153 return result.set(h('PageHeading', [
52- h('h1', 'Cannot display message.')
54+ h('h1', i18n('Cannot display message.'))
5355 ]))
5456 }
5557
5658 // what happens in private stays in private!
@@ -66,9 +68,9 @@
6668 meta.branch.set(isReply ? thread.branchId : thread.lastId)
6769
6870 var container = h('Thread', [
6971 h('div.messages', [
70- when(thread.branchId, h('a.full', {href: thread.rootId}, ['View full thread'])),
72+ when(thread.branchId, h('a.full', {href: thread.rootId}, [i18n('View full thread')])),
7173 map(thread.messages, (msg) => {
7274 return computed([msg, thread.previousKey(msg)], (msg, previousId) => {
7375 return api.message.html.render(msg, {pageId: id, previousId, includeReferences: true})
7476 })
modules/page/html/render/private.jsView
@@ -4,14 +4,16 @@
44 exports.needs = nest({
55 'feed.html.rollup': 'first',
66 'feed.pull.private': 'first',
77 'message.html.compose': 'first',
8- 'keys.sync.id': 'first'
8+ 'keys.sync.id': 'first',
9+ 'intl.sync.i18n': 'first',
910 })
1011
1112 exports.gives = nest('page.html.render')
1213
1314 exports.create = function (api) {
15+ const i18n = api.intl.sync.i18n
1416 return nest('page.html.render', function channel (path) {
1517 if (path !== '/private') return
1618
1719 var id = api.keys.sync.id()
@@ -23,9 +25,9 @@
2325 return ref.isFeed(typeof e === 'string' ? e : e.link)
2426 })
2527 return msg
2628 },
27- placeholder: `Write a private message \n\n\n\nThis can only be read by yourself and people you have @mentioned.`
29+ placeholder: i18n('Write a private message')
2830 })
2931 ]
3032
3133 return api.feed.html.rollup(api.feed.pull.private, { prepend })
modules/page/html/render/profile.jsView
@@ -25,13 +25,15 @@
2525 'profile.sheet.edit': 'first',
2626 'contact.obs': {
2727 followers: 'first',
2828 following: 'first'
29- }
29+ },
30+ 'intl.sync.i18n': 'first',
3031 })
3132 exports.gives = nest('page.html.render')
3233
3334 exports.create = function (api) {
35+ const i18n = api.intl.sync.i18n
3436 return nest('page.html.render', function profile (id) {
3537 if (!ref.isFeed(id)) return
3638
3739 var name = api.about.obs.name(id)
@@ -83,9 +85,9 @@
8385 classList: [
8486 when(isSelf, '-self'),
8587 when(isAssigned, '-assigned')
8688 ],
87- title: nameList(when(isSelf, 'Self Assigned', 'Assigned By'), item.value)
89+ title: nameList(when(isSelf, i18n('Self Assigned'), i18n('Assigned By')), item.value)
8890 }, [
8991 item.key
9092 ])
9193 }),
@@ -111,9 +113,9 @@
111113 classList: [
112114 when(isSelf, '-self'),
113115 when(isAssigned, '-assigned')
114116 ],
115- title: nameList(when(isSelf, 'Self Assigned', 'Assigned By'), item.value)
117+ title: nameList(when(isSelf, i18n('Self Assigned'), i18n('Assigned By')), item.value)
116118 }, [
117119 h('img', {
118120 className: 'Avatar',
119121 style: { 'background-color': api.about.obs.color(id) },
@@ -137,20 +139,20 @@
137139 h('div.title', [
138140 h('h1', [name]),
139141 h('div.meta', [
140142 when(id === yourId, [
141- h('button', {'ev-click': api.profile.sheet.edit}, 'Edit Your Profile')
143+ h('button', {'ev-click': api.profile.sheet.edit}, i18n('Edit Your Profile'))
142144 ], [
143145 when(youFollow,
144146 h('a.ToggleButton.-unsubscribe', {
145147 'href': '#',
146- 'title': 'Click to unfollow',
148+ 'title': i18n('Click to unfollow'),
147149 'ev-click': send(unfollow, id)
148- }, when(isFriends, 'Friends', 'Following')),
150+ }, when(isFriends, i18n('Friends'), i18n('Following'))),
149151 h('a.ToggleButton.-subscribe', {
150152 'href': '#',
151153 'ev-click': send(follow, id)
152- }, when(followsYou, 'Follow Back', 'Follow'))
154+ }, when(followsYou, i18n('Follow Back'), i18n('Follow')))
153155 )
154156 ])
155157 ])
156158 ]),
@@ -177,11 +179,11 @@
177179 ]),
178180 h('div.side.-right', [
179181 when(friendsLoaded,
180182 h('div', [
181- renderContactBlock('Friends', friends, yourFollows),
182- renderContactBlock('Followers', followers, yourFollows),
183- renderContactBlock('Following', following, yourFollows)
183+ renderContactBlock(i18n('Friends'), friends, yourFollows),
184+ renderContactBlock(i18n('Followers'), followers, yourFollows),
185+ renderContactBlock(i18n('Following'), following, yourFollows)
184186 ]),
185187 h('div', {className: 'Loading'})
186188 )
187189 ])
@@ -273,9 +275,9 @@
273275 h('h2', {
274276 style: {
275277 'font-weight': 'normal'
276278 }
277- }, ['What whould you like to call ', h('strong', [currentName]), '?']),
279+ }, [i18n('What whould you like to call '), h('strong', [currentName]), '?']),
278280 input
279281 ]),
280282 footer: [
281283 h('button -save', {
@@ -289,12 +291,12 @@
289291 })
290292 }
291293 close()
292294 }
293- }, 'Confirm'),
295+ }, i18n('Confirm')),
294296 h('button -cancel', {
295297 'ev-click': close
296- }, 'Cancel')
298+ }, i18n('Cancel'))
297299 ]
298300 }
299301 })
300302 }
modules/page/html/render/public.jsView
@@ -28,16 +28,18 @@
2828 subscribed: 'first',
2929 recent: 'first'
3030 },
3131 'keys.sync.id': 'first',
32- 'settings.obs.get': 'first'
32+ 'settings.obs.get': 'first',
33+ 'intl.sync.i18n': 'first',
3334 })
3435
3536 exports.gives = nest({
3637 'page.html.render': true
3738 })
3839
3940 exports.create = function (api) {
41+ const i18n = api.intl.sync.i18n
4042 return nest('page.html.render', page)
4143
4244 function page (path) {
4345 if (path !== '/public') return // "/" is a sigil for "page"
@@ -52,9 +54,9 @@
5254 var localPeers = api.sbot.obs.localPeers()
5355 var connectedPubs = computed([connectedPeers, localPeers], (c, l) => c.filter(x => !l.includes(x)))
5456
5557 var prepend = [
56- api.message.html.compose({ meta: { type: 'post' }, placeholder: 'Write a public message' })
58+ api.message.html.compose({ meta: { type: 'post' }, placeholder: i18n('Write a public message') })
5759 ]
5860
5961 var getStream = (opts) => {
6062 if (opts.lt != null && !opts.lt.marker) {
@@ -123,11 +125,11 @@
123125 })
124126 return [
125127 h('button -pub -full', {
126128 'ev-click': api.invite.sheet
127- }, '+ Join Pub'),
128- when(loading, [ h('Loading') ], [
129- when(computed(channels, x => x.length), h('h2', 'Active Channels')),
129+ }, i18n('+ Join Pub')),
130+ when(loading, [ h("Loading") ], [
131+ when(computed(channels, x => x.length), h('h2', i18n("Active Channels"))),
130132 h('div', {
131133 classList: 'ChannelList',
132134 hidden: loading
133135 }, [
@@ -142,23 +144,23 @@
142144 h('span.name', '#' + channel),
143145 when(subscribed,
144146 h('a.-unsubscribe', {
145147 'ev-click': send(unsubscribe, channel)
146- }, 'Unsubscribe'),
148+ }, i18n('Unsubscribe')),
147149 h('a.-subscribe', {
148150 'ev-click': send(subscribe, channel)
149- }, 'Subscribe')
151+ }, i18n('Subscribe'))
150152 )
151153 ])
152154 }, {maxTime: 5}),
153- h('a.channel -more', {href: '/channels'}, 'More Channels...')
155+ h('a.channel -more', {href: '/channels'}, i18n('More Channels...'))
154156 ])
155157 ]),
156158
157- PeerList(localPeers, 'Local'),
158- PeerList(connectedPubs, 'Connected Pubs'),
159+ PeerList(localPeers, i18n('Local')),
160+ PeerList(connectedPubs, i18n('Connected Pubs')),
159161
160- when(computed(whoToFollow, x => x.length), h('h2', 'Who to follow')),
162+ when(computed(whoToFollow, x => x.length), h('h2', i18n('Who to follow'))),
161163 when(following.sync,
162164 h('div', {
163165 classList: 'ProfileList'
164166 }, [
modules/page/html/render/search.jsView
@@ -10,14 +10,16 @@
1010
1111 exports.needs = nest({
1212 'sbot.pull.stream': 'first',
1313 'keys.sync.id': 'first',
14- 'message.html.render': 'first'
14+ 'message.html.render': 'first',
15+ 'intl.sync.i18n': 'first'
1516 })
1617
1718 exports.gives = nest('page.html.render')
1819
1920 exports.create = function (api) {
21+ const i18n = api.intl.sync.i18n
2022 return nest('page.html.render', function channel (path) {
2123 if (path[0] !== '?') return
2224
2325 var queryStr = path.substr(1).trim()
@@ -28,13 +30,13 @@
2830 var updates = Value(0)
2931 var aborter = null
3032
3133 const searchHeader = h('div', {className: 'PageHeading'}, [
32- h('h1', [h('strong', 'Search Results:'), ' ', query.join(' ')])
34+ h('h1', [h('strong', i18n('Search Results:')), ' ', query.join(' ')])
3335 ])
3436
3537 var updateLoader = h('a Notifier -loader', { href: '#', 'ev-click': refresh }, [
36- 'Show ', h('strong', [updates]), ' ', plural(updates, 'update', 'updates')
38+ 'Show ', h('strong', [updates]), ' ', plural(updates, i18n('update'), i18n('updates'))
3739 ])
3840
3941 var content = Proxy()
4042 var container = h('Scroller', {
@@ -48,9 +50,9 @@
4850 style: {
4951 'padding': '60px 0',
5052 'font-size': '150%'
5153 }
52- }, [h('strong', 'Search completed.'), ' ', count, ' ', plural(count, 'result', 'results'), ' found']))
54+ }, [h('strong', i18n('Search completed.')), ' ', count, ' ', plural(count, i18n('result found'), i18n('results found'))]))
5355 ])
5456 ])
5557 ])
5658
modules/page/html/render/settings.jsView
@@ -1,59 +1,87 @@
11 var { h, computed } = require('mutant')
22 var nest = require('depnest')
3+var appRoot = require('app-root-path')
34
45 var themeNames = Object.keys(require('../../../../styles'))
56
67 exports.needs = nest({
78 'settings.obs.get': 'first',
89 'settings.sync.set': 'first',
10+ 'intl.sync.locales': 'first',
11+ 'intl.sync.i18n': 'first'
912 })
1013
1114 exports.gives = nest('page.html.render')
1215
1316 exports.create = function (api) {
1417 return nest('page.html.render', function channel (path) {
1518 if (path !== '/settings') return
16-
19+ const i18n = api.intl.sync.i18n
20+
1721 const currentTheme = api.settings.obs.get('patchwork.theme')
22+ const currentLang = api.settings.obs.get('patchwork.lang')
23+ const langNames = api.intl.sync.locales()
1824 const filterFollowing = api.settings.obs.get('filters.following')
1925
2026 var prepend = [
2127 h('PageHeading', [
2228 h('h1', [
23- h('strong', 'Settings')
24- ])
29+ h('strong', i18n('Settings'))
30+ ]),
2531 ])
2632 ]
2733
2834 return h('Scroller', { style: { overflow: 'auto' } }, [
2935 h('div.wrapper', [
3036 h('section.prepend', prepend),
3137 h('section.content', [
3238 h('section', [
33- h('h2', 'Theme'),
34- h('select', {
35- style: {
36- 'font-size': '120%'
37- },
38- value: currentTheme,
39- 'ev-change': (ev) => api.settings.sync.set({
40- patchwork: {theme: ev.target.value}
39+ h('h2', i18n('Theme')),
40+ computed(currentTheme, currentTheme => {
41+ return themeNames.map(name => {
42+ const style = currentTheme == name
43+ ? { 'margin-right': '1rem', 'border-color': 'teal' }
44+ : { 'margin-right': '1rem' }
45+
46+ return h('button', {
47+ 'ev-click': () => api.settings.sync.set({
48+ patchwork: {theme: name}
49+ }),
50+ style
51+ }, name)
4152 })
4253 }, [
4354 themeNames.map(name => h('option', {value: name}, [name]))
4455 ])
4556 ]),
4657 h('section', [
47- h('h2', 'Filters'),
58+ h('h2', i18n('Language')),
59+ computed(currentLang, currentLang => {
60+ return langNames.map(lang => {
61+ const style = currentLang == lang
62+ ? { 'margin-right': '1rem', 'border-color': 'teal' }
63+ : { 'margin-right': '1rem' }
64+
65+ return h('button', {
66+ 'ev-click': () => api.settings.sync.set({
67+ patchwork: {lang: lang}
68+ }),
69+ style
70+ }, lang)
71+ })
72+ })
73+ ]),
74+ h('section', [
75+ h('h2', i18n('Filters')),
4876 h('label', [
4977 h('input', {
5078 type: 'checkbox',
5179 checked: filterFollowing,
5280 'ev-change': (ev) => api.settings.sync.set({
5381 filters: {following: ev.target.checked}
5482 })
55- }), ' Hide following messages'
83+ }), i18n(' Hide following messages')
5684 ])
5785 ])
5886 ])
5987 ])
modules/profile/sheet/edit.jsView
@@ -15,12 +15,14 @@
1515 image: 'first',
1616 color: 'first'
1717 },
1818 'blob.html.input': 'first',
19- 'blob.sync.url': 'first'
19+ 'blob.sync.url': 'first',
20+ 'intl.sync.i18n': 'first',
2021 })
2122
2223 exports.create = function (api) {
24+ const i18n = api.intl.sync.i18n
2325 return nest('profile.sheet.edit', function () {
2426 var id = api.keys.sync.id()
2527 api.sheet.display(close => {
2628 var currentName = api.about.obs.name(id)
@@ -44,17 +46,17 @@
4446 h('h2', {
4547 style: {
4648 'font-weight': 'normal'
4749 }
48- }, ['Your Profile']),
50+ }, [i18n('Your Profile')]),
4951 h('ProfileEditor', [
5052 h('div.side', [
5153 h('ImageInput', [
5254 h('img', {
5355 style: { 'background-color': api.about.obs.color(id) },
5456 src: computed(chosenImage, (id) => id ? api.blob.sync.url(id) : fallbackImageUrl)
5557 }),
56- h('span', ['🖼 Choose Profile Image...']),
58+ h('span', ['🖼 ', i18n('Choose Profile Image...')]),
5759 api.blob.html.input(file => {
5860 chosenImage.set(file.link)
5961 }, {
6062 accept: 'image/*',
@@ -63,13 +65,13 @@
6365 ])
6466 ]),
6567 h('div.main', [
6668 h('input.name', {
67- placeholder: 'Choose a name',
69+ placeholder: i18n('Choose a name'),
6870 hooks: [ValueHook(chosenName), FocusHook()]
6971 }),
7072 h('textarea.description', {
71- placeholder: 'Describe yourself (if you want)',
73+ placeholder: i18n('Describe yourself (if you want)'),
7274 hooks: [ValueHook(chosenDescription)]
7375 })
7476 ])
7577 ])
@@ -77,12 +79,12 @@
7779 footer: [
7880 h('button -save', {
7981 'ev-click': save,
8082 'disabled': publishing
81- }, when(publishing, 'Publishing...', 'Publish')),
83+ }, when(publishing, i18n('Publishing...'), i18n('Publish'))),
8284 h('button -cancel', {
8385 'ev-click': close
84- }, 'Cancel')
86+ }, i18n('Cancel'))
8587 ]
8688 }
8789
8890 function save () {
@@ -103,11 +105,11 @@
103105 if (err) {
104106 publishing.set(false)
105107 showDialog({
106108 type: 'error',
107- title: 'Error',
108- buttons: ['OK'],
109- message: 'An error occurred while attempting to publish about message.',
109+ title: i18n('Error'),
110+ buttons: [i18n('OK')],
111+ message: i18n('An error occurred while attempting to publish about message.'),
110112 detail: err.message
111113 })
112114 } else {
113115 close()
overrides/patchcore/lib/timeAgo.jsView
@@ -1,0 +1,42 @@
1+const Value = require('mutant/value')
2+const computed = require('mutant/computed')
3+const nest = require('depnest')
4+const human = require('human-time')
5+
6+exports.gives = nest('lib.obs.timeAgo')
7+
8+exports.needs = nest({
9+ 'intl.sync.time': 'first'
10+})
11+
12+exports.create = function (api) {
13+ return nest('lib.obs.timeAgo', timeAgo)
14+
15+ function timeAgo (timestamp) {
16+ var timer
17+ var value = Value(TimeIntl(timestamp))
18+ return computed([value], (a) => a, {
19+ onListen: () => {
20+ timer = setInterval(refresh, 30e3)
21+ refresh()
22+ },
23+ onUnlisten: () => {
24+ clearInterval(timer)
25+ }
26+ }, {
27+ idle: true
28+ })
29+
30+ function refresh () {
31+ value.set(TimeIntl(timestamp))
32+ }
33+
34+ function TimeIntl(timestamp) {
35+ return api.intl.sync.time(Time(timestamp))
36+ }
37+ }
38+}
39+
40+function Time (timestamp) {
41+ return human(new Date(timestamp))
42+}
package.jsonView
@@ -13,8 +13,9 @@
1313 },
1414 "author": "Secure Scuttlebutt Consortium",
1515 "license": "AGPL-3.0",
1616 "dependencies": {
17+ "app-root-path": "^2.0.1",
1718 "bulk-require": "^1.0.0",
1819 "compare-version": "^0.1.2",
1920 "cross-script": "^1.0.1",
2021 "deep-equal": "^1.0.1",
@@ -26,8 +27,9 @@
2627 "fix-path": "^2.1.0",
2728 "flatpickr": "^3.0.5-1",
2829 "flumeview-level": "^2.0.3",
2930 "hashlru": "^2.2.0",
31+ "i18n": "^0.8.3",
3032 "insert-css": "~2.0.0",
3133 "level": "~1.7.0",
3234 "lrucache": "^1.0.2",
3335 "micro-css": "^2.0.1",
plugs/message/html/layout/default.jsView
@@ -13,14 +13,16 @@
1313 action: 'map',
1414 timestamp: 'first',
1515 backlinks: 'first'
1616 },
17- 'about.html.image': 'first'
17+ 'about.html.image': 'first',
18+ 'intl.sync.i18n': 'first',
1819 })
1920
2021 exports.gives = nest('message.html.layout')
2122
2223 exports.create = function (api) {
24+ const i18n = api.intl.sync.i18n
2325 return nest('message.html.layout', layout)
2426
2527 function layout (msg, {layout, previousId, priority, content, includeReferences = false}) {
2628 if (!(layout === undefined || layout === 'default')) return
@@ -32,13 +34,13 @@
3234 classList.push('-reply')
3335 var branch = msg.value.content.branch
3436 if (branch) {
3537 if (!previousId || (previousId && last(branch) && previousId !== last(branch))) {
36- replyInfo = h('span', ['in reply to ', api.message.html.link(last(branch))])
38+ replyInfo = h('span', [i18n('in reply to '), api.message.html.link(last(branch))])
3739 }
3840 }
3941 } else if (msg.value.content.project) {
40- replyInfo = h('span', ['on ', api.message.html.link(msg.value.content.project)])
42+ replyInfo = h('span', [i18n('on '), api.message.html.link(msg.value.content.project)])
4143 }
4244
4345 if (priority === 2) {
4446 classList.push('-new')
@@ -65,9 +67,9 @@
6567
6668 function messageHeader (msg, {replyInfo, priority}) {
6769 var additionalMeta = []
6870 if (priority >= 2) {
69- additionalMeta.push(h('span.flag -new', {title: 'New Message'}))
71+ additionalMeta.push(h('span.flag -new', {title: i18n('New Message')}))
7072 }
7173 return h('header', [
7274 h('div.main', [
7375 h('a.avatar', {href: `${msg.value.author}`}, [
plugs/message/html/meta/likes.jsView
@@ -1,14 +1,17 @@
11 var nest = require('depnest')
22 var { h, computed, map, send } = require('mutant')
3+
34 exports.gives = nest('message.html.meta')
45 exports.needs = nest({
56 'message.obs.likes': 'first',
67 'message.sheet.likes': 'first',
7- 'about.obs.name': 'first'
8+ 'about.obs.name': 'first',
9+ 'intl.sync.i18n': 'first',
810 })
911
1012 exports.create = function (api) {
13+ const i18n = api.intl.sync.i18n
1114 return nest('message.html.meta', function likes (msg) {
1215 if (msg.key) {
1316 return computed(api.message.obs.likes(msg.key), likeCount)
1417 }
@@ -19,15 +22,15 @@
1922 return [' ', h('a.likes', {
2023 title: names(likes),
2124 href: '#',
2225 'ev-click': send(api.message.sheet.likes, likes)
23- }, [`${likes.length} ${likes.length === 1 ? 'like' : 'likes'}`])]
26+ }, [`${likes.length} ${likes.length === 1 ? i18n('like') : i18n('likes')}`])]
2427 }
2528 }
2629
2730 function names (ids) {
2831 var items = map(ids, api.about.obs.name)
2932 return computed([items], (names) => {
30- return 'Liked by\n' + names.map((n) => `- ${n}`).join('\n')
33+ return i18n('Liked by\n') + names.map((n) => `- ${n}`).join('\n')
3134 })
3235 }
3336 }
plugs/message/html/render/about.jsView
@@ -12,14 +12,16 @@
1212 },
1313 'keys.sync.id': 'first',
1414 'profile.html.person': 'first',
1515 'about.obs.name': 'first',
16- 'blob.sync.url': 'first'
16+ 'blob.sync.url': 'first',
17+ 'intl.sync.i18n': 'first',
1718 })
1819
1920 exports.gives = nest('message.html.render')
2021
2122 exports.create = function (api) {
23+ const i18n = api.intl.sync.i18n
2224 return nest('message.html.render', function about (msg, opts) {
2325 if (msg.value.content.type !== 'about') return
2426 if (!ref.isFeed(msg.value.content.about)) return
2527
@@ -32,20 +34,20 @@
3234 if (c.name) {
3335 var target = api.profile.html.person(c.about, c.name)
3436 miniContent.push(computed([self, api.about.obs.name(c.about), c.name], (self, a, b) => {
3537 if (self) {
36- return ['self identifies as "', target, '"']
38+ return [i18n('self identifies as "'), target, '"']
3739 } else if (a === b) {
38- return ['identified ', api.profile.html.person(c.about)]
40+ return [i18n('identified '), api.profile.html.person(c.about)]
3941 } else {
40- return ['identifies ', api.profile.html.person(c.about), ' as "', target, '"']
42+ return [i18n('identifies '), api.profile.html.person(c.about), i18n(' as "'), target, '"']
4143 }
4244 }))
4345 }
4446
4547 if (c.image) {
4648 if (!miniContent.length) {
47- var imageAction = self ? 'self assigned a display image' : ['assigned a display image to ', api.profile.html.person(c.about)]
49+ var imageAction = self ? i18n('self assigned a display image') : [i18n('assigned a display image to '), api.profile.html.person(c.about)]
4850 miniContent.push(imageAction)
4951 }
5052
5153 content.push(h('a AboutImage', {
@@ -69,9 +71,9 @@
6971
7072 if (c.description) {
7173 elements.push(api.message.html.decorate(api.message.html.layout(msg, extend({
7274 showActions: true,
73- miniContent: self ? 'self assigned a description' : ['assigned a description to ', api.profile.html.person(c.about)],
75+ miniContent: self ? i18n('self assigned a description') : [i18n('assigned a description to '), api.profile.html.person(c.about)],
7476 content: api.message.html.markdown(c.description),
7577 layout: 'mini'
7678 }, opts)), { msg }))
7779 }
plugs/message/html/render/channel.jsView
@@ -5,14 +5,16 @@
55 exports.needs = nest({
66 'message.html': {
77 decorate: 'reduce',
88 layout: 'first'
9- }
9+ },
10+ 'intl.sync.i18n':'first',
1011 })
1112
1213 exports.gives = nest('message.html.render')
1314
1415 exports.create = function (api) {
16+ const i18n = api.intl.sync.i18n
1517 return nest('message.html.render', function renderMessage (msg, opts) {
1618 if (msg.value.content.type !== 'channel') return
1719 var element = api.message.html.layout(msg, extend({
1820 miniContent: messageContent(msg),
@@ -25,9 +27,9 @@
2527 function messageContent (msg) {
2628 var channel = `#${msg.value.content.channel}`
2729 var subscribed = msg.value.content.subscribed
2830 return [
29- subscribed ? 'subscribed to ' : 'unsubscribed from ',
31+ subscribed ? i18n('subscribed to ') : i18n('unsubscribed from '),
3032 h('a', {href: channel}, channel)
3133 ]
3234 }
3335 }
plugs/message/html/render/following.jsView
@@ -7,14 +7,16 @@
77 'message.html': {
88 decorate: 'reduce',
99 layout: 'first'
1010 },
11- 'profile.html.person': 'first'
11+ 'profile.html.person': 'first',
12+ 'intl.sync.i18n': 'first',
1213 })
1314
1415 exports.gives = nest('message.html.render')
1516
1617 exports.create = function (api) {
18+ const i18n = api.intl.sync.i18n
1719 return nest('message.html.render', function renderMessage (msg, opts) {
1820 if (msg.value.content.type !== 'contact') return
1921 if (!ref.isFeed(msg.value.content.contact)) return
2022 if (typeof msg.value.content.following !== 'boolean') return
@@ -29,9 +31,9 @@
2931
3032 function messageContent (msg) {
3133 var following = msg.value.content.following
3234 return [
33- following ? 'followed ' : 'unfollowed ',
35+ following ? i18n('followed ') : i18n('unfollowed '),
3436 api.profile.html.person(msg.value.content.contact)
3537 ]
3638 }
3739 }
plugs/intl/sync/i18n.jsView
@@ -1,0 +1,99 @@
1+const nest = require('depnest')
2+var { watch } = require('mutant')
3+var appRoot = require('app-root-path');
4+var i18nL = require("i18n")
5+
6+exports.gives = nest('intl.sync', [
7+ 'locale',
8+ 'locales',
9+ 'i18n',
10+ 'time',
11+])
12+
13+exports.needs = nest({
14+ 'intl.sync.locale':'first',
15+ 'intl.sync.locales':'reduce',
16+ 'settings.obs.get': 'first',
17+ 'settings.sync.set': 'first'
18+})
19+
20+exports.create = (api) => {
21+ let _locale
22+
23+ const {
24+ locale: getLocale,
25+ locales: getLocales,
26+ i18n: getI18n,
27+ } = api.intl.sync
28+
29+ return nest('intl.sync', {
30+ locale,
31+ locales,
32+ i18n,
33+ time
34+ })
35+
36+ //Get locale value in setting
37+ function locale () {
38+ return api.settings.obs.get('patchwork.lang')
39+ }
40+
41+ //Get all locales loaded in i18nL
42+ function locales (sofar = {}) {
43+ return i18nL.getLocales()
44+ }
45+
46+ //Get translation
47+ function i18n (value) {
48+ _init()
49+ return i18nL.__(value)
50+ }
51+
52+ function time (date){
53+ return date
54+ .replace(/from now/, i18n('form now'))
55+ .replace(/ago/, i18n('ago'))
56+ .replace(/years/,i18n('years'))
57+ .replace(/months/,i18n('months'))
58+ .replace(/weeks/,i18n('weeks'))
59+ .replace(/days/,i18n('days'))
60+ .replace(/hours/,i18n('hours'))
61+ .replace(/minutes/,i18n('minutes'))
62+ .replace(/seconds/,i18n('seconds'))
63+ .replace(/year/,i18n('year'))
64+ .replace(/month/,i18n('month'))
65+ .replace(/week/,i18n('week'))
66+ .replace(/day/,i18n('day'))
67+ .replace(/hour/,i18n('hour'))
68+ .replace(/minute/,i18n('minute'))
69+ .replace(/second/,i18n('second'))
70+ }
71+
72+ //Init an subscribe to settings changes.
73+ function _init() {
74+ if (_locale) return
75+ //TODO: Depject this!
76+ i18nL.configure({
77+ directory: appRoot + '/locales',
78+ defaultLocale: 'en'
79+ });
80+
81+ watch(api.settings.obs.get('patchwork.lang',navigator.language), currentLocale => {
82+ i18nL.setLocale(getSubLocal(currentLocale))
83+
84+ // Only refresh if the language has already been selected once.
85+ // This will prevent the update loop
86+ if (_locale) {
87+ electron.remote.getCurrentWebContents().reloadIgnoringCache()
88+ }
89+ })
90+
91+ _locale = true;
92+ }
93+
94+}
95+
96+//For now get only global languages
97+function getSubLocal(loc) {
98+ return loc.split('-')[0]
99+}
locales/en.jsonView
@@ -1,0 +1,136 @@
1+{
2+ "Patchwork": "Patchwork",
3+ "Public": "Public",
4+ "Private": "Private",
5+ "Write a public message": "Write a public message",
6+ "Active Channels": "Active Channels",
7+ "Loading": "Loading",
8+ "Local": "Local",
9+ "Connected Pubs": "Connected Pubs",
10+ "Who to follow": "Who to follow",
11+ "Unsubscribe": "Unsubscribe",
12+ "Subscribe": "Subscribe",
13+ "Publishing...": "Publishing...",
14+ "Publish": "Publish",
15+ "Show ": "Show ",
16+ "update": "update",
17+ "updates": "updates",
18+ "+ Join Pub": "+ Join Pub",
19+ "word, @key, #channel": "word, @key, #channel",
20+ "Profile": "Profile",
21+ "Mentions": "Mentions",
22+ " liked this message": " liked this message",
23+ "View full thread": "View full thread",
24+ " replied": " replied",
25+ " replied to ": " replied to ",
26+ "like": "like",
27+ "Liked by\n": "Liked by\n",
28+ " and ": " and ",
29+ " followed ": " followed ",
30+ " subscribed to ": " subscribed to ",
31+ "likes": "likes",
32+ " others": " others",
33+ "Write a private reply": "Write a private reply",
34+ "Write a public reply": "Write a public reply",
35+ "More Channels...": "More Channels...",
36+ "Cannot display message.": "Cannot display message.",
37+ "All Posts from Your ": "All Posts from Your ",
38+ "Click to unsubscribe": "Click to unsubscribe",
39+ "Subscribed": "Subscribed",
40+ "Write a message in this channel": "Write a message in this channel\n\n\n\nPeople who follow you or subscribe to this channel will also see this message in their main feed.\n\nTo create a new channel, type the channel name (preceded by a #) into the search box above. e.g #cat-pics",
41+ "liked this message": "liked this message",
42+ "replied to this message": "replied to this message",
43+ "added changes": "added changes",
44+ "mentioned you": "mentioned you",
45+ "mentioned this channel": "mentioned this channel",
46+ "Write a private message": "Write a private message \n\n\n\nThis can only be read by yourself and people you have @mentioned.",
47+ "Edit Your Profile": "Edit Your Profile",
48+ "Click to unfollow": "Click to unfollow",
49+ "Friends": "Friends",
50+ "Following": "Following",
51+ "Follow Back": "Follow Back",
52+ "Follow": "Follow",
53+ "Followers": "Followers",
54+ "More": "More",
55+ "Gatherings": "Gatherings",
56+ "Extended Network": "Extended Network",
57+ "Settings": "Settings",
58+ "Upgrading database": "Upgrading database",
59+ "Downloading new messages": "Downloading new messages",
60+ "Indexing database": "Indexing database",
61+ "Scuttling...": "Scuttling...",
62+ " has been released.": " has been released.",
63+ " Click here to download and view more info!": " Click here to download and view more info!",
64+ "Self Assigned": "Self Assigned",
65+ "Assigned By": "Assigned By",
66+ "self assigned a description": "self assigned a description",
67+ "in reply to ": "in reply to ",
68+ "subscribed to ": "subscribed to ",
69+ "followed ": "followed ",
70+ "identifies ": "identifies ",
71+ " as \"": " as \"",
72+ "paste invite code here": "paste invite code here",
73+ "By default, Patchwork will only see other users that are on the same local area network as you.": "By default, Patchwork will only see other users that are on the same local area network as you.",
74+ "In order to share with users on the internet, you need to be invited to a pub server.": "In order to share with users on the internet, you need to be invited to a pub server.",
75+ "Redeem Invite": "Redeem Invite",
76+ "Cancel": "Cancel",
77+ "Channels": "Channels",
78+ "Browse All": "Browse All",
79+ " from your extended network": " from your extended network",
80+ "+ Add Gathering": "+ Add Gathering",
81+ " referenced this message:": " referenced this message:",
82+ "Create": "Create",
83+ " Gathering": " Gathering",
84+ "Choose a title": "Choose a title",
85+ "Choose date and time": "Choose date and time",
86+ "Choose Banner Image...": "Choose Banner Image...",
87+ "Describe the gathering (if you want)": "Describe the gathering (if you want)",
88+ "Edit": "Edit",
89+ "identified ": "identified ",
90+ "self identifies as \"": "self identifies as \"",
91+ "form now": "form now",
92+ "ago": "ago",
93+ "years": "years",
94+ "months": "months",
95+ "weeks": "weeks",
96+ "days": "days",
97+ "hours": "hours",
98+ "minutes": "mins",
99+ "seconds": "secs",
100+ "year": "year",
101+ "month": "month",
102+ "week": "week",
103+ "day": "day",
104+ "hour": "hour",
105+ "minute": "min",
106+ "second": "sec",
107+ "assigned a display image to ": "assigned a display image to ",
108+ "Theme": "Theme",
109+ "Language": "Language",
110+ "Filters": "Filters",
111+ " Hide following messages": " Hide following messages",
112+ "Cannot display message" : "Cannot display message",
113+ "Search Results:" : "Search Results:",
114+ "Search completed." : "Search completed.",
115+ "result found" : "result found",
116+ "results found" : "results found",
117+ " forked this discussion:" : " forked this discussion:",
118+ "Your Profile" : "Your Profile",
119+ "Choose Profile Image..." : "Choose Profile Image...",
120+ "Choose a name" : "Choose a name",
121+ "Describe yourself (if you want)" : "Describe yourself (if you want)",
122+ "What whould you like to call " : "What whould you like to call ",
123+ "Confirm" : "Confirm",
124+ "self assigned a display image" : "self assigned a display image",
125+ "Like" : "Like",
126+ "Reply" : "Reply",
127+ "unfollowed " : "unfollowed ",
128+ "Untitled Gathering" : "Untitled Gathering",
129+ "Error" : "Error",
130+ "An error occurred while attempting to publish gathering." : "An error occurred while attempting to publish gathering.",
131+ "An error occurred while attempting to redeem invite." : "An error occurred while attempting to redeem invite.",
132+ "OK" : "OK",
133+ "Close" : "Close",
134+ "New Message" : "New Message",
135+ "unsubscribed from " : "unsubscribed from "
136+}
locales/es.jsonView
@@ -1,0 +1,136 @@
1+{
2+ "Patchwork": "Patchwork",
3+ "Public": "Público",
4+ "Private": "Privado",
5+ "Write a public message": "Escribe un mensaje públio",
6+ "Active Channels": "Canales activos",
7+ "Loading": "Cargando",
8+ "Local": "Local",
9+ "Connected Pubs": "Pubs conectados",
10+ "Who to follow": "A quién seguir",
11+ "Unsubscribe": "Anular suscripción",
12+ "Subscribe": "Suscribir",
13+ "Publishing...": "Publicando...",
14+ "Publish": "Publicar",
15+ "Show ": "Mostrar ",
16+ "update": "actualización",
17+ "updates": "actualizaciones",
18+ "+ Join Pub": "+ Unirce a un Pub",
19+ "word, @key, #channel": "palabra, @key, #canal",
20+ "Profile": "Perfil",
21+ "Mentions": "Menciones",
22+ " liked this message": " le gustó este mensaje",
23+ "View full thread": "Ver hilo completo",
24+ " replied": " respondió",
25+ " replied to ": " respondió a ",
26+ "like": "Me gusta",
27+ "Liked by\n": "Les gusta a\n",
28+ " and ": " y ",
29+ " followed ": " siguie a ",
30+ "subscribed to ": "se suscribió a ",
31+ " subscribed to ": "se suscribió a ",
32+ "likes": "Me gusta",
33+ " others": " más",
34+ "Write a private reply": "Escriba una respuesta privada",
35+ "Write a public reply": "Escriba una respuesta pública",
36+ "Cannot display message": "No se puede mostrar el mensaje",
37+ "Cannot display message.": "No se puede mostrar el mensaje.",
38+ "More Channels...": "Más canales...",
39+ "Click to unsubscribe": "Click para anular suscripción",
40+ "Subscribed": "Suscrito",
41+ "Write a message in this channel": "Escriba un mensaje en este canal \n\n\n\nLas personas que te siguen o esten suscritas a este canal también verán este mensaje en su feed principal.\n\nPara crear un nuevo canal, escribe el nombre del canal (precedido por un #) en el cuadro de búsqueda de arriba, por ejemplo #cat-pics",
42+ "Gatherings": "Reuniones",
43+ " from your extended network": " de su red extendida",
44+ "+ Add Gathering": "+ Agregar Reunión",
45+ "Write a private message": "Write a private message \n\n\n\nThis can only be read by yourself and people you have @mentioned.",
46+ "Edit Your Profile": "Edite su perfil",
47+ "Click to unfollow": "Click para dejar de seguir",
48+ "Friends": "Amigos",
49+ "Following": "Siguiendo",
50+ "Follow Back": "Seguir de vuelta",
51+ "Follow": "Seguir",
52+ "Followers": "Seguidores",
53+ "Self Assigned": "Auto asignado",
54+ "Assigned By": "Asignado por",
55+ "Search Results:": "Resultados de busqueda:",
56+ "Search completed.": "Busqueda completada.",
57+ "result found": "resultado encontrado",
58+ "results found": "resultados encontrados",
59+ "Settings": "Configuraciones",
60+ "Theme": "Diseño",
61+ "Filters": "Filtros",
62+ " Hide following messages": " Ocultar mensajes de seguidores",
63+ "Upgrading database": "Upgrading database",
64+ "Downloading new messages": "Downloading new messages",
65+ "Indexing database": "Indexing database",
66+ "Scuttling...": "Scuttling...",
67+ "Create": "Crear",
68+ " Gathering": " Reunión",
69+ "Choose a title": "Elige un título",
70+ "Choose date and time": "Elige una fecha y hora",
71+ "Choose Banner Image...": "Elige una imagen destacada...",
72+ "Describe the gathering (if you want)": "Describa la reunión (si lo desea)",
73+ "Cancel": "Cancelar",
74+ "paste invite code here": "pegar el código de invitación",
75+ "By default, Patchwork will only see other users that are on the same local area network as you.": "Por defecto Patchwork sólo verá a otros usuarios que estén en la misma red de área local que usted.",
76+ "In order to share with users on the internet, you need to be invited to a pub server.": "Para compartir con los usuarios en Internet, es necesario ser invitado a un servidor de pub.",
77+ "Redeem Invite": "Validar invitación",
78+ " forked this discussion:": " bifurcó esta discucion:",
79+ "All Posts from Your ": "Todos los mensajes de su ",
80+ " referenced this message:": " hizo referencia a este mensaje:",
81+ "Your Profile": "Su perfil",
82+ "Choose Profile Image...": "Elegir imagen de visualización...",
83+ "Choose a name": "Elige un nombre",
84+ "Describe yourself (if you want)": "Descríbase (si quiere)",
85+ "in reply to ": "en respuesta a ",
86+ "self assigned a description": "se asignó una descripción",
87+ "identifies ": "identificó a ",
88+ "What whould you like to call ": "Cómo te gustaría llamar a ",
89+ "Confirm": "Confirmar",
90+ "self assigned a display image": "se asignó una imagen de visualización",
91+ "self identifies as \"": "autoidentificado como \"",
92+ "Like": "Me gusta",
93+ "Reply": "Comentar",
94+ "followed ": "sigue a ",
95+ "identified ": "se denominó ",
96+ "unfollowed ": "unfollowed ",
97+ " as \"": " como \"",
98+ "assigned a display image to ": "asignó una imagen de visualización a ",
99+ "liked this message": "le gusto este mensaje",
100+ "replied to this message": "comentó en este mensaje",
101+ "added changes": "agregó cambios",
102+ "mentioned you": "te mencionó",
103+ "mentioned this channel": "mencionó este canal",
104+ "Extended Network": "Red extendida",
105+ " has been released.": " a sido publicada.",
106+ " Click here to download and view more info!": " Haga clic aquí para descargar y ver más información!",
107+ "Channels": "Canales",
108+ "Browse All": "Ver todos",
109+ "More": "Más",
110+ "Edit": "Editar",
111+ "Untitled Gathering": "Reunión sin título",
112+ "Error": "Error",
113+ "An error occurred while attempting to publish gathering.": "Se ha producido un error al intentar publicar la reunión",
114+ "An error occurred while attempting to redeem invite.": "Se ha producido un error al intentar validar la invitación.",
115+ "OK": "OK",
116+ "Close": "Cerrar",
117+ "New Message": "Nuevo mensaje",
118+ "Language": "Idioma",
119+ "form now": "en el futuro",
120+ "ago": "atras",
121+ "years": "años",
122+ "months": "meses",
123+ "weeks": "semanas",
124+ "days": "días",
125+ "hours": "horas",
126+ "minutes": "minutos",
127+ "seconds": "segundos",
128+ "year": "año",
129+ "month": "mes",
130+ "week": "semana",
131+ "day": "día",
132+ "hour": "hora",
133+ "minute": "minutos",
134+ "second": "segundos",
135+ "unsubscribed from ": "desuscrito de "
136+}
locales/ki.jsonView
@@ -1,0 +1,37 @@
1+{
2+ "Write a public message": "پبلک پیغام لکھنے",
3+ "Active Channels": "فعال",
4+ "Loading": "لوڈ ہو رہا ہے",
5+ "Local": "مقامی",
6+ "Connected Pubs": "مربوط عوامی مقامات",
7+ "Who to follow": "جو پیروی کرنے کے لئے",
8+ "Public": "عوام",
9+ "Private": "نجی",
10+ "Patchwork": "जन्द । नक्तकः",
11+ "Unsubscribe": "رکنیت ختم",
12+ "Subscribe": "سبسکرائب",
13+ "+ Join Pub": "+ عوامی جگہ شمولیت",
14+ "Profile": "تم",
15+ "Mentions": "تذکرے",
16+ "Show ": "شو ",
17+ "update": "اپ ڈیٹ",
18+ "updates": "اپ ڈیٹ",
19+ " liked this message": " اس پیغام کو پسند کیا",
20+ "View full thread": "مکمل دھاگے کو دیکھیں",
21+ " replied": " جواب",
22+ " subscribed to ": " سبسکرائب ",
23+ " replied to ": " کا جواب دیا ",
24+ " followed ": " کی پیروی کی ",
25+ " and ": " اور ",
26+ " others": " دوسروں",
27+ " liked ": " پسند کیا ",
28+ "word, @key, #channel": "لفظ, @کلید, #چینل",
29+ "like": "کی طرح",
30+ "Liked by\n": "پسند\n",
31+ "likes": "پسند کرتا ہے",
32+ "Write a private reply": "نجی جواب",
33+ "Write a public reply": "پبلک جواب",
34+ "Publishing...": "پبلشنگ ...",
35+ "Publish": "شائع",
36+ "More Channels...": "More Channels..."
37+}

Built with git-ssb-web