signalk-chiplog 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/README.md +290 -0
- package/index.js +327 -0
- package/lib/api.js +427 -0
- package/lib/database.js +264 -0
- package/lib/detection.js +407 -0
- package/lib/entries.js +345 -0
- package/lib/errors.js +22 -0
- package/lib/event-watcher.js +332 -0
- package/lib/events.js +287 -0
- package/lib/export.js +220 -0
- package/lib/formats.js +215 -0
- package/lib/manoeuvre-types.js +84 -0
- package/lib/observation-recorder.js +152 -0
- package/lib/place-names.js +208 -0
- package/lib/places.js +128 -0
- package/lib/propulsion-detector.js +156 -0
- package/lib/propulsion.js +75 -0
- package/lib/rows.js +15 -0
- package/lib/track-recorder.js +149 -0
- package/lib/track.js +58 -0
- package/lib/usb-scheduler.js +131 -0
- package/lib/validation.js +105 -0
- package/package.json +67 -0
- package/public/app.css +603 -0
- package/public/entry/entry.css +528 -0
- package/public/entry/icons/apple-touch-icon.png +0 -0
- package/public/entry/icons/icon-192.png +0 -0
- package/public/entry/icons/icon-512.png +0 -0
- package/public/entry/index.html +22 -0
- package/public/entry/js/access.mjs +116 -0
- package/public/entry/js/clock.mjs +29 -0
- package/public/entry/js/components/AccessGate.mjs +70 -0
- package/public/entry/js/components/Dialogs.mjs +64 -0
- package/public/entry/js/components/ManoeuvrePad.mjs +106 -0
- package/public/entry/js/components/NotePanel.mjs +34 -0
- package/public/entry/js/components/RecentList.mjs +113 -0
- package/public/entry/js/components/SketchPanel.mjs +181 -0
- package/public/entry/js/components/StatusHeader.mjs +69 -0
- package/public/entry/js/journal.mjs +139 -0
- package/public/entry/js/main.mjs +360 -0
- package/public/entry/js/outbox.mjs +124 -0
- package/public/entry/js/strokes.mjs +81 -0
- package/public/entry/manifest.webmanifest +26 -0
- package/public/entry/sw.js +75 -0
- package/public/icon.svg +7 -0
- package/public/index.html +18 -0
- package/public/js/api.mjs +85 -0
- package/public/js/auth.mjs +41 -0
- package/public/js/components/ExportView.mjs +182 -0
- package/public/js/components/LogView.mjs +132 -0
- package/public/js/components/PassageView.mjs +343 -0
- package/public/js/components/PropulsionStrip.mjs +60 -0
- package/public/js/components/StatusBar.mjs +55 -0
- package/public/js/components/Timeline.mjs +226 -0
- package/public/js/components/TrackMap.mjs +85 -0
- package/public/js/components/common.mjs +70 -0
- package/public/js/context.mjs +56 -0
- package/public/js/days.mjs +56 -0
- package/public/js/format.mjs +84 -0
- package/public/js/i18n.mjs +480 -0
- package/public/js/ids.mjs +10 -0
- package/public/js/main.mjs +48 -0
- package/public/js/status.mjs +21 -0
- package/public/vendor/htm-LICENSE +202 -0
- package/public/vendor/leaflet/LICENSE +26 -0
- package/public/vendor/leaflet/images/layers-2x.png +0 -0
- package/public/vendor/leaflet/images/layers.png +0 -0
- package/public/vendor/leaflet/images/marker-icon-2x.png +0 -0
- package/public/vendor/leaflet/images/marker-icon.png +0 -0
- package/public/vendor/leaflet/images/marker-shadow.png +0 -0
- package/public/vendor/leaflet/leaflet.css +661 -0
- package/public/vendor/leaflet/leaflet.js +6 -0
- package/public/vendor/preact-LICENSE +21 -0
- package/public/vendor/preact-htm.mjs +1 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
// Every string the webapp shows lives here, in each supported language. A test
|
|
2
|
+
// checks that both dictionaries have the same keys and placeholders.
|
|
3
|
+
|
|
4
|
+
export const MESSAGES = {
|
|
5
|
+
en: {
|
|
6
|
+
'app.title': 'Chiplog',
|
|
7
|
+
'nav.log': 'Logbook',
|
|
8
|
+
'nav.export': 'Export',
|
|
9
|
+
'footer.attribution':
|
|
10
|
+
'Map data and place names © OpenStreetMap contributors (ODbL) · Seamarks © OpenSeaMap',
|
|
11
|
+
|
|
12
|
+
'unit.knots': 'kn',
|
|
13
|
+
'unit.nauticalMiles': 'nm',
|
|
14
|
+
|
|
15
|
+
'common.loading': 'Loading…',
|
|
16
|
+
'common.retry': 'Retry',
|
|
17
|
+
'common.save': 'Save',
|
|
18
|
+
'common.cancel': 'Cancel',
|
|
19
|
+
'common.edit': 'Edit',
|
|
20
|
+
|
|
21
|
+
'error.forbidden':
|
|
22
|
+
'Your Signal K account is not allowed to do this. Sign in with an account that has the rights.',
|
|
23
|
+
'error.signIn': 'Open the Signal K admin',
|
|
24
|
+
'error.notRunning': 'The Chiplog plugin is not running. Enable it in the Signal K admin.',
|
|
25
|
+
'error.network': 'The Signal K server cannot be reached.',
|
|
26
|
+
'error.generic': 'Something went wrong: {message}',
|
|
27
|
+
|
|
28
|
+
'status.underwaySail': 'Under way under sail',
|
|
29
|
+
'status.underwayEngine': 'Under way under engine',
|
|
30
|
+
'status.underway': 'Under way',
|
|
31
|
+
'status.stopped': 'Stopped',
|
|
32
|
+
'status.unknown': 'Waiting for data',
|
|
33
|
+
'status.currentPassage': 'Current passage',
|
|
34
|
+
'status.fallback':
|
|
35
|
+
'Detected from speed alone: install signalk-autostate for more reliable detection.',
|
|
36
|
+
'status.fallbackPending':
|
|
37
|
+
'Detected from speed until signalk-autostate makes its first decision.',
|
|
38
|
+
'status.fallbackStale':
|
|
39
|
+
'Detected from speed alone: navigation.state from {source} has not been updated since {time}.',
|
|
40
|
+
'status.fallbackUnrecognised':
|
|
41
|
+
'Detected from speed alone: navigation.state is “{value}” (from {source}), a value Chiplog does not use.',
|
|
42
|
+
|
|
43
|
+
'log.title': 'Logbook',
|
|
44
|
+
'log.empty':
|
|
45
|
+
'No passage recorded yet. Passages are logged automatically once the vessel gets under way.',
|
|
46
|
+
'log.loadOlder': 'Load older passages',
|
|
47
|
+
'log.inProgress': 'in progress',
|
|
48
|
+
'log.dayDistance': '{distance} sailed',
|
|
49
|
+
'log.fromPreviousDay': 'continued from the previous day',
|
|
50
|
+
'log.toNextDay': 'continues the next day',
|
|
51
|
+
|
|
52
|
+
'place.unknown': 'Unnamed place',
|
|
53
|
+
'place.pending': 'Provisional name: waiting for geocoding',
|
|
54
|
+
|
|
55
|
+
'passage.back': 'Back to the logbook',
|
|
56
|
+
'passage.notFound': 'This passage does not exist.',
|
|
57
|
+
'passage.inProgress': 'In progress',
|
|
58
|
+
'passage.stoppedSince': 'Stopped since {time}',
|
|
59
|
+
'passage.departure': 'Departure',
|
|
60
|
+
'passage.arrival': 'Arrival',
|
|
61
|
+
'passage.distance': 'Distance',
|
|
62
|
+
'passage.duration': 'Duration',
|
|
63
|
+
'passage.underway': 'Under way',
|
|
64
|
+
'passage.engine': 'Engine',
|
|
65
|
+
'passage.sail': 'Sail',
|
|
66
|
+
'passage.averageSpeed': 'Average speed',
|
|
67
|
+
'passage.track': 'Track',
|
|
68
|
+
'passage.noTrack': 'No track recorded for this passage.',
|
|
69
|
+
'passage.downloadGpx': 'Download the GPX track',
|
|
70
|
+
'passage.propulsion': 'Engine and sail',
|
|
71
|
+
'passage.noPropulsion': 'No engine or sail period recorded.',
|
|
72
|
+
'passage.rpm': '{rpm} rpm',
|
|
73
|
+
'passage.log': 'Log',
|
|
74
|
+
'passage.noLog': 'Nothing logged yet.',
|
|
75
|
+
|
|
76
|
+
'map.seamarks': 'Seamarks',
|
|
77
|
+
|
|
78
|
+
'timeline.time': 'Time',
|
|
79
|
+
'timeline.position': 'Position',
|
|
80
|
+
'timeline.speed': 'Speed',
|
|
81
|
+
'timeline.course': 'Course',
|
|
82
|
+
'timeline.wind': 'Wind',
|
|
83
|
+
'timeline.depth': 'Depth',
|
|
84
|
+
'timeline.pressure': 'Baro',
|
|
85
|
+
'timeline.remarks': 'Remarks',
|
|
86
|
+
'timeline.apparent': 'app.',
|
|
87
|
+
'timeline.heading': 'hdg',
|
|
88
|
+
|
|
89
|
+
'observation.entry_start': 'Departure',
|
|
90
|
+
'observation.entry_end': 'Arrival',
|
|
91
|
+
'observation.periodic': 'Reading',
|
|
92
|
+
'observation.event': 'Reading',
|
|
93
|
+
|
|
94
|
+
'event.handwritten': 'Handwritten note',
|
|
95
|
+
'event.sail': 'sail: {sail}',
|
|
96
|
+
'event.alarm': 'Alarm: {message}',
|
|
97
|
+
'event.alarmCleared': 'Alarm cleared: {message}',
|
|
98
|
+
'event.autopilotEngaged': 'Autopilot engaged',
|
|
99
|
+
'event.autopilotDisengaged': 'Autopilot disengaged',
|
|
100
|
+
'event.autopilotMode': 'Autopilot mode: {mode}',
|
|
101
|
+
'event.windAbove': 'Wind above {threshold} (average {speed})',
|
|
102
|
+
'event.windBelow': 'Wind back below {threshold}',
|
|
103
|
+
'event.pressureDrop': 'Barometer down {drop} over 3 h',
|
|
104
|
+
'event.correction': 'Corrected: {before} → {after}',
|
|
105
|
+
|
|
106
|
+
'type.engine': 'engine',
|
|
107
|
+
'type.sail': 'sail',
|
|
108
|
+
|
|
109
|
+
'manoeuvre.tack': 'Tack',
|
|
110
|
+
'manoeuvre.gybe': 'Gybe',
|
|
111
|
+
'manoeuvre.reef_in': 'Reef in',
|
|
112
|
+
'manoeuvre.reef_out': 'Shake out reef',
|
|
113
|
+
'manoeuvre.sail_change': 'Sail change',
|
|
114
|
+
'manoeuvre.anchor_down': 'Anchor down',
|
|
115
|
+
'manoeuvre.anchor_up': 'Anchor up',
|
|
116
|
+
'manoeuvre.moor': 'Moor',
|
|
117
|
+
'manoeuvre.cast_off': 'Cast off',
|
|
118
|
+
'manoeuvre.watch_change': 'Watch change',
|
|
119
|
+
|
|
120
|
+
'corrections.title': 'Corrections',
|
|
121
|
+
'corrections.renameHint':
|
|
122
|
+
'A name you set is remembered and used for later departures and arrivals nearby.',
|
|
123
|
+
'corrections.switchTo': 'Switch to {type}',
|
|
124
|
+
'corrections.close': 'Close this passage',
|
|
125
|
+
'corrections.closeConfirm':
|
|
126
|
+
'Close this passage now? If the vessel has already stopped, the passage ends when it stopped.',
|
|
127
|
+
'corrections.mergePrevious': 'Merge with the previous passage',
|
|
128
|
+
'corrections.mergeNext': 'Merge with the next passage',
|
|
129
|
+
'corrections.mergeConfirm': 'Merge with “{other}”? The two passages will become one.',
|
|
130
|
+
'corrections.delete': 'Delete this passage',
|
|
131
|
+
'corrections.deleteConfirm':
|
|
132
|
+
'Delete this passage, its track and its log lines? This cannot be undone.',
|
|
133
|
+
|
|
134
|
+
'export.title': 'Export',
|
|
135
|
+
'export.intro': 'Download the logbook, whole or for a period.',
|
|
136
|
+
'export.from': 'From',
|
|
137
|
+
'export.to': 'To',
|
|
138
|
+
'export.allHint': 'Leave the dates empty to export everything.',
|
|
139
|
+
'export.invalidRange': 'The end date cannot be before the start date.',
|
|
140
|
+
'export.json': 'JSON — the complete record',
|
|
141
|
+
'export.csv': 'CSV — logbook lines for a spreadsheet',
|
|
142
|
+
'export.gpx': 'GPX — tracks',
|
|
143
|
+
'export.pdf': 'PDF — logbook facsimile, coming in V1.1',
|
|
144
|
+
'export.usbTitle': 'USB drive',
|
|
145
|
+
'export.usbIntro':
|
|
146
|
+
'Copy the logbook to the USB drive set in the plugin configuration — one JSON, CSV and GPX file per passage — so it can be recovered if the vessel has to be abandoned.',
|
|
147
|
+
'export.usbWrite': 'Write to the USB drive now',
|
|
148
|
+
'export.usbWritten':
|
|
149
|
+
'{written} passages written, {unchanged} already up to date, in {directory}',
|
|
150
|
+
'export.usbNotConfigured': 'No USB drive directory is set. Add it in the plugin configuration.',
|
|
151
|
+
'export.usbUnavailable':
|
|
152
|
+
'The USB drive directory is not available: is the drive plugged in and mounted?',
|
|
153
|
+
'export.pluginConfiguration': 'Plugin configuration',
|
|
154
|
+
'export.usbScheduleBoth': 'Copied automatically every {minutes} min and at each arrival.',
|
|
155
|
+
'export.usbScheduleInterval': 'Copied automatically every {minutes} min.',
|
|
156
|
+
'export.usbScheduleArrival': 'Copied automatically at each arrival.',
|
|
157
|
+
'export.usbScheduleNone': 'No automatic copy: the drive is only written from here.',
|
|
158
|
+
'export.usbLastCopy':
|
|
159
|
+
'Last copy {time}: {written} passages written, {unchanged} already up to date.',
|
|
160
|
+
'export.usbNeverCopied': 'No copy made since the plugin started.',
|
|
161
|
+
'export.usbLastError': 'The last copy failed, {time}: {message}',
|
|
162
|
+
'export.usbNextCopy': 'Next automatic copy around {time}.',
|
|
163
|
+
|
|
164
|
+
'nav.entry': 'Helm entry',
|
|
165
|
+
'entry.title': 'Chiplog helm',
|
|
166
|
+
'entry.logbook': 'Logbook',
|
|
167
|
+
'entry.night': 'Night',
|
|
168
|
+
'entry.online': 'Connected',
|
|
169
|
+
'entry.offline': 'Not connected',
|
|
170
|
+
'entry.queued': '{count} waiting to be sent',
|
|
171
|
+
'entry.noPassage': 'No passage open',
|
|
172
|
+
'entry.waitingToLeave': 'Ready to leave since {time}',
|
|
173
|
+
'entry.passageFrom': 'From {place} · {duration}',
|
|
174
|
+
'entry.departHint': 'Cast off or weigh anchor to open a passage.',
|
|
175
|
+
'entry.manoeuvres': 'Manoeuvres',
|
|
176
|
+
'entry.note': 'Note',
|
|
177
|
+
'entry.sketch': 'Handwriting',
|
|
178
|
+
'entry.notePlaceholder': 'What happened?',
|
|
179
|
+
'entry.send': 'Log it',
|
|
180
|
+
'entry.logged': 'Logged at {time}',
|
|
181
|
+
'entry.loggedQueued': 'Kept on this tablet, sent once connected',
|
|
182
|
+
'entry.openedPassage': 'Passage opened',
|
|
183
|
+
'entry.undo': 'Undo',
|
|
184
|
+
'entry.undone': 'Undone',
|
|
185
|
+
'entry.addComment': 'Add a comment',
|
|
186
|
+
'entry.comment': 'Comment',
|
|
187
|
+
'entry.noPassageError': 'No passage to log this in: cast off or weigh anchor first.',
|
|
188
|
+
'entry.refused': 'Not logged: {message}',
|
|
189
|
+
'entry.chooseSail': 'Which sail is up?',
|
|
190
|
+
'entry.otherSail': 'Other sail',
|
|
191
|
+
'entry.sketchUndo': 'Undo stroke',
|
|
192
|
+
'entry.sketchClear': 'Clear',
|
|
193
|
+
'entry.sketchArea': 'Handwriting area',
|
|
194
|
+
'entry.sketchHint': 'Write or draw here',
|
|
195
|
+
'entry.recent': 'Latest entries',
|
|
196
|
+
'entry.recentEmpty': 'Nothing logged in this passage yet.',
|
|
197
|
+
'entry.pending': 'Waiting to be sent',
|
|
198
|
+
'entry.failed': 'Not logged: {reason}',
|
|
199
|
+
'entry.discard': 'Discard',
|
|
200
|
+
'entry.delete': 'Delete',
|
|
201
|
+
'entry.deleteConfirm': 'Delete this entry from the logbook?',
|
|
202
|
+
'access.title': 'This tablet needs access',
|
|
203
|
+
'access.intro': 'Signal K security is on: logging entries needs read/write access.',
|
|
204
|
+
'access.request': 'Request access for this tablet',
|
|
205
|
+
'access.pending':
|
|
206
|
+
'Request sent. Approve it in the Signal K admin, under Security → Access Requests, with read/write permission.',
|
|
207
|
+
'access.approved': 'Access granted.',
|
|
208
|
+
'access.denied': 'The request was denied.',
|
|
209
|
+
'access.expired': 'The request expired. Ask again.',
|
|
210
|
+
'access.disabled':
|
|
211
|
+
'This server does not accept device access requests. Allow them in the Signal K admin (Security → Settings), or sign in.',
|
|
212
|
+
'access.unavailable': 'Signal K security is off, so there is no access to request.',
|
|
213
|
+
'access.error': 'The request could not be sent.',
|
|
214
|
+
'access.signIn': 'Sign in instead',
|
|
215
|
+
'access.cancel': 'Cancel the request',
|
|
216
|
+
'access.deviceName': 'Chiplog tablet',
|
|
217
|
+
'sail.main': 'Mainsail',
|
|
218
|
+
'sail.genoa': 'Genoa',
|
|
219
|
+
'sail.jib': 'Jib',
|
|
220
|
+
'sail.staysail': 'Staysail',
|
|
221
|
+
'sail.spinnaker': 'Spinnaker',
|
|
222
|
+
'sail.gennaker': 'Gennaker',
|
|
223
|
+
'sail.code0': 'Code 0',
|
|
224
|
+
'sail.storm_jib': 'Storm jib'
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
fr: {
|
|
228
|
+
'app.title': 'Chiplog',
|
|
229
|
+
'nav.log': 'Journal',
|
|
230
|
+
'nav.export': 'Export',
|
|
231
|
+
'footer.attribution':
|
|
232
|
+
'Données cartographiques et noms de lieux © contributeurs OpenStreetMap (ODbL) · Balisage © OpenSeaMap',
|
|
233
|
+
|
|
234
|
+
'unit.knots': 'nd',
|
|
235
|
+
'unit.nauticalMiles': 'M',
|
|
236
|
+
|
|
237
|
+
'common.loading': 'Chargement…',
|
|
238
|
+
'common.retry': 'Réessayer',
|
|
239
|
+
'common.save': 'Enregistrer',
|
|
240
|
+
'common.cancel': 'Annuler',
|
|
241
|
+
'common.edit': 'Modifier',
|
|
242
|
+
|
|
243
|
+
'error.forbidden':
|
|
244
|
+
'Votre compte Signal K n’a pas les droits pour cette action. Connectez-vous avec un compte autorisé.',
|
|
245
|
+
'error.signIn': 'Ouvrir l’administration Signal K',
|
|
246
|
+
'error.notRunning':
|
|
247
|
+
'Le plugin Chiplog n’est pas démarré. Activez-le dans l’administration Signal K.',
|
|
248
|
+
'error.network': 'Le serveur Signal K est injoignable.',
|
|
249
|
+
'error.generic': 'Une erreur est survenue : {message}',
|
|
250
|
+
|
|
251
|
+
'status.underwaySail': 'En route à la voile',
|
|
252
|
+
'status.underwayEngine': 'En route au moteur',
|
|
253
|
+
'status.underway': 'En route',
|
|
254
|
+
'status.stopped': 'À l’arrêt',
|
|
255
|
+
'status.unknown': 'En attente de données',
|
|
256
|
+
'status.currentPassage': 'Navigation en cours',
|
|
257
|
+
'status.fallback':
|
|
258
|
+
'Détection d’après la vitesse seule : installez signalk-autostate pour une détection plus fiable.',
|
|
259
|
+
'status.fallbackPending':
|
|
260
|
+
'Détection d’après la vitesse en attendant la première décision de signalk-autostate.',
|
|
261
|
+
'status.fallbackStale':
|
|
262
|
+
'Détection d’après la vitesse seule : navigation.state (source {source}) n’a pas été mis à jour depuis {time}.',
|
|
263
|
+
'status.fallbackUnrecognised':
|
|
264
|
+
'Détection d’après la vitesse seule : navigation.state vaut « {value} » (source {source}), une valeur que Chiplog n’utilise pas.',
|
|
265
|
+
|
|
266
|
+
'log.title': 'Journal de bord',
|
|
267
|
+
'log.empty':
|
|
268
|
+
'Aucune navigation enregistrée pour l’instant. Elles sont notées automatiquement dès que le bateau fait route.',
|
|
269
|
+
'log.loadOlder': 'Charger les navigations plus anciennes',
|
|
270
|
+
'log.inProgress': 'en cours',
|
|
271
|
+
'log.dayDistance': '{distance} parcourus',
|
|
272
|
+
'log.fromPreviousDay': 'suite de la veille',
|
|
273
|
+
'log.toNextDay': 'se poursuit le lendemain',
|
|
274
|
+
|
|
275
|
+
'place.unknown': 'Lieu sans nom',
|
|
276
|
+
'place.pending': 'Nom provisoire : en attente du géocodage',
|
|
277
|
+
|
|
278
|
+
'passage.back': 'Retour au journal',
|
|
279
|
+
'passage.notFound': 'Cette navigation n’existe pas.',
|
|
280
|
+
'passage.inProgress': 'En cours',
|
|
281
|
+
'passage.stoppedSince': 'À l’arrêt depuis {time}',
|
|
282
|
+
'passage.departure': 'Départ',
|
|
283
|
+
'passage.arrival': 'Arrivée',
|
|
284
|
+
'passage.distance': 'Distance',
|
|
285
|
+
'passage.duration': 'Durée',
|
|
286
|
+
'passage.underway': 'En route',
|
|
287
|
+
'passage.engine': 'Moteur',
|
|
288
|
+
'passage.sail': 'Voile',
|
|
289
|
+
'passage.averageSpeed': 'Vitesse moyenne',
|
|
290
|
+
'passage.track': 'Trace',
|
|
291
|
+
'passage.noTrack': 'Aucune trace enregistrée pour cette navigation.',
|
|
292
|
+
'passage.downloadGpx': 'Télécharger la trace GPX',
|
|
293
|
+
'passage.propulsion': 'Moteur et voile',
|
|
294
|
+
'passage.noPropulsion': 'Aucune période moteur ou voile enregistrée.',
|
|
295
|
+
'passage.rpm': '{rpm} tr/min',
|
|
296
|
+
'passage.log': 'Journal',
|
|
297
|
+
'passage.noLog': 'Rien de noté pour l’instant.',
|
|
298
|
+
|
|
299
|
+
'map.seamarks': 'Balisage',
|
|
300
|
+
|
|
301
|
+
'timeline.time': 'Heure',
|
|
302
|
+
'timeline.position': 'Position',
|
|
303
|
+
'timeline.speed': 'Vitesse',
|
|
304
|
+
'timeline.course': 'Route',
|
|
305
|
+
'timeline.wind': 'Vent',
|
|
306
|
+
'timeline.depth': 'Fond',
|
|
307
|
+
'timeline.pressure': 'Baro',
|
|
308
|
+
'timeline.remarks': 'Observations',
|
|
309
|
+
'timeline.apparent': 'app.',
|
|
310
|
+
'timeline.heading': 'cap',
|
|
311
|
+
|
|
312
|
+
'observation.entry_start': 'Départ',
|
|
313
|
+
'observation.entry_end': 'Arrivée',
|
|
314
|
+
'observation.periodic': 'Relevé',
|
|
315
|
+
'observation.event': 'Relevé',
|
|
316
|
+
|
|
317
|
+
'event.handwritten': 'Note manuscrite',
|
|
318
|
+
'event.sail': 'voile : {sail}',
|
|
319
|
+
'event.alarm': 'Alarme : {message}',
|
|
320
|
+
'event.alarmCleared': 'Fin d’alarme : {message}',
|
|
321
|
+
'event.autopilotEngaged': 'Pilote automatique embrayé',
|
|
322
|
+
'event.autopilotDisengaged': 'Pilote automatique débrayé',
|
|
323
|
+
'event.autopilotMode': 'Mode du pilote : {mode}',
|
|
324
|
+
'event.windAbove': 'Vent au-dessus de {threshold} (moyenne {speed})',
|
|
325
|
+
'event.windBelow': 'Vent retombé sous {threshold}',
|
|
326
|
+
'event.pressureDrop': 'Baromètre en baisse de {drop} en 3 h',
|
|
327
|
+
'event.correction': 'Corrigé : {before} → {after}',
|
|
328
|
+
|
|
329
|
+
'type.engine': 'moteur',
|
|
330
|
+
'type.sail': 'voile',
|
|
331
|
+
|
|
332
|
+
'manoeuvre.tack': 'Virement de bord',
|
|
333
|
+
'manoeuvre.gybe': 'Empannage',
|
|
334
|
+
'manoeuvre.reef_in': 'Prise de ris',
|
|
335
|
+
'manoeuvre.reef_out': 'Largage de ris',
|
|
336
|
+
'manoeuvre.sail_change': 'Changement de voile',
|
|
337
|
+
'manoeuvre.anchor_down': 'Mouillage',
|
|
338
|
+
'manoeuvre.anchor_up': 'Ancre levée',
|
|
339
|
+
'manoeuvre.moor': 'Amarrage',
|
|
340
|
+
'manoeuvre.cast_off': 'Appareillage',
|
|
341
|
+
'manoeuvre.watch_change': 'Changement de quart',
|
|
342
|
+
|
|
343
|
+
'corrections.title': 'Corrections',
|
|
344
|
+
'corrections.renameHint':
|
|
345
|
+
'Un nom saisi est retenu et réutilisé pour les départs et arrivées suivants à proximité.',
|
|
346
|
+
'corrections.switchTo': 'Passer en {type}',
|
|
347
|
+
'corrections.close': 'Clore cette navigation',
|
|
348
|
+
'corrections.closeConfirm':
|
|
349
|
+
'Clore cette navigation maintenant ? Si le bateau est déjà arrêté, elle se termine au moment de l’arrêt.',
|
|
350
|
+
'corrections.mergePrevious': 'Fusionner avec la navigation précédente',
|
|
351
|
+
'corrections.mergeNext': 'Fusionner avec la navigation suivante',
|
|
352
|
+
'corrections.mergeConfirm':
|
|
353
|
+
'Fusionner avec « {other} » ? Les deux navigations n’en feront plus qu’une.',
|
|
354
|
+
'corrections.delete': 'Supprimer cette navigation',
|
|
355
|
+
'corrections.deleteConfirm':
|
|
356
|
+
'Supprimer cette navigation, sa trace et ses lignes de journal ? C’est irréversible.',
|
|
357
|
+
|
|
358
|
+
'export.title': 'Export',
|
|
359
|
+
'export.intro': 'Téléchargez le journal, en entier ou sur une période.',
|
|
360
|
+
'export.from': 'Du',
|
|
361
|
+
'export.to': 'Au',
|
|
362
|
+
'export.allHint': 'Laissez les dates vides pour tout exporter.',
|
|
363
|
+
'export.invalidRange': 'La date de fin ne peut pas précéder la date de début.',
|
|
364
|
+
'export.json': 'JSON — l’enregistrement complet',
|
|
365
|
+
'export.csv': 'CSV — les lignes du journal, pour un tableur',
|
|
366
|
+
'export.gpx': 'GPX — les traces',
|
|
367
|
+
'export.pdf': 'PDF — fac-similé du journal, prévu en V1.1',
|
|
368
|
+
'export.usbTitle': 'Clé USB',
|
|
369
|
+
'export.usbIntro':
|
|
370
|
+
'Copier le journal sur la clé USB indiquée dans la configuration du plugin — un fichier JSON, CSV et GPX par navigation — pour le récupérer en cas d’abandon du navire.',
|
|
371
|
+
'export.usbWrite': 'Écrire sur la clé USB maintenant',
|
|
372
|
+
'export.usbWritten': '{written} navigations écrites, {unchanged} déjà à jour, dans {directory}',
|
|
373
|
+
'export.usbNotConfigured':
|
|
374
|
+
'Aucun dossier de clé USB n’est indiqué. Ajoutez-le dans la configuration du plugin.',
|
|
375
|
+
'export.usbUnavailable':
|
|
376
|
+
'Le dossier de la clé USB est indisponible : la clé est-elle branchée et montée ?',
|
|
377
|
+
'export.pluginConfiguration': 'Configuration du plugin',
|
|
378
|
+
'export.usbScheduleBoth': 'Copie automatique toutes les {minutes} min et à chaque arrivée.',
|
|
379
|
+
'export.usbScheduleInterval': 'Copie automatique toutes les {minutes} min.',
|
|
380
|
+
'export.usbScheduleArrival': 'Copie automatique à chaque arrivée.',
|
|
381
|
+
'export.usbScheduleNone':
|
|
382
|
+
'Pas de copie automatique : la clé n’est écrite que depuis cette page.',
|
|
383
|
+
'export.usbLastCopy':
|
|
384
|
+
'Dernière copie {time} : {written} navigations écrites, {unchanged} déjà à jour.',
|
|
385
|
+
'export.usbNeverCopied': 'Aucune copie depuis le démarrage du plugin.',
|
|
386
|
+
'export.usbLastError': 'La dernière copie a échoué, {time} : {message}',
|
|
387
|
+
'export.usbNextCopy': 'Prochaine copie automatique vers {time}.',
|
|
388
|
+
|
|
389
|
+
'nav.entry': 'Saisie à bord',
|
|
390
|
+
'entry.title': 'Chiplog à bord',
|
|
391
|
+
'entry.logbook': 'Journal',
|
|
392
|
+
'entry.night': 'Nuit',
|
|
393
|
+
'entry.online': 'Connecté',
|
|
394
|
+
'entry.offline': 'Hors connexion',
|
|
395
|
+
'entry.queued': '{count} en attente d’envoi',
|
|
396
|
+
'entry.noPassage': 'Aucune navigation ouverte',
|
|
397
|
+
'entry.waitingToLeave': 'Prêt à partir depuis {time}',
|
|
398
|
+
'entry.passageFrom': 'Depuis {place} · {duration}',
|
|
399
|
+
'entry.departHint': 'Appareillez ou levez l’ancre pour ouvrir une navigation.',
|
|
400
|
+
'entry.manoeuvres': 'Manœuvres',
|
|
401
|
+
'entry.note': 'Note',
|
|
402
|
+
'entry.sketch': 'Manuscrit',
|
|
403
|
+
'entry.notePlaceholder': 'Que s’est-il passé ?',
|
|
404
|
+
'entry.send': 'Consigner',
|
|
405
|
+
'entry.logged': 'Consigné à {time}',
|
|
406
|
+
'entry.loggedQueued': 'Gardé sur la tablette, envoyé au retour de la connexion',
|
|
407
|
+
'entry.openedPassage': 'Navigation ouverte',
|
|
408
|
+
'entry.undo': 'Annuler',
|
|
409
|
+
'entry.undone': 'Annulé',
|
|
410
|
+
'entry.addComment': 'Ajouter un commentaire',
|
|
411
|
+
'entry.comment': 'Commentaire',
|
|
412
|
+
'entry.noPassageError':
|
|
413
|
+
'Aucune navigation où consigner : appareillez ou levez l’ancre d’abord.',
|
|
414
|
+
'entry.refused': 'Non consigné : {message}',
|
|
415
|
+
'entry.chooseSail': 'Quelle voile est envoyée ?',
|
|
416
|
+
'entry.otherSail': 'Autre voile',
|
|
417
|
+
'entry.sketchUndo': 'Effacer le trait',
|
|
418
|
+
'entry.sketchClear': 'Tout effacer',
|
|
419
|
+
'entry.sketchArea': 'Zone d’écriture',
|
|
420
|
+
'entry.sketchHint': 'Écrivez ou dessinez ici',
|
|
421
|
+
'entry.recent': 'Dernières saisies',
|
|
422
|
+
'entry.recentEmpty': 'Rien de consigné dans cette navigation.',
|
|
423
|
+
'entry.pending': 'En attente d’envoi',
|
|
424
|
+
'entry.failed': 'Non consigné : {reason}',
|
|
425
|
+
'entry.discard': 'Abandonner',
|
|
426
|
+
'entry.delete': 'Supprimer',
|
|
427
|
+
'entry.deleteConfirm': 'Supprimer cette saisie du journal ?',
|
|
428
|
+
'access.title': 'Cette tablette a besoin d’un accès',
|
|
429
|
+
'access.intro':
|
|
430
|
+
'La sécurité Signal K est active : consigner demande un accès en lecture-écriture.',
|
|
431
|
+
'access.request': 'Demander l’accès pour cette tablette',
|
|
432
|
+
'access.pending':
|
|
433
|
+
'Demande envoyée. Approuvez-la dans l’admin Signal K, rubrique Security → Access Requests, en lecture-écriture.',
|
|
434
|
+
'access.approved': 'Accès accordé.',
|
|
435
|
+
'access.denied': 'La demande a été refusée.',
|
|
436
|
+
'access.expired': 'La demande a expiré. Recommencez.',
|
|
437
|
+
'access.disabled':
|
|
438
|
+
'Ce serveur n’accepte pas les demandes d’accès des appareils. Autorisez-les dans l’admin Signal K (Security → Settings), ou connectez-vous.',
|
|
439
|
+
'access.unavailable': 'La sécurité Signal K est désactivée : aucun accès à demander.',
|
|
440
|
+
'access.error': 'La demande n’a pas pu être envoyée.',
|
|
441
|
+
'access.signIn': 'Se connecter plutôt',
|
|
442
|
+
'access.cancel': 'Annuler la demande',
|
|
443
|
+
'access.deviceName': 'Tablette Chiplog',
|
|
444
|
+
'sail.main': 'Grand-voile',
|
|
445
|
+
'sail.genoa': 'Génois',
|
|
446
|
+
'sail.jib': 'Foc',
|
|
447
|
+
'sail.staysail': 'Trinquette',
|
|
448
|
+
'sail.spinnaker': 'Spi',
|
|
449
|
+
'sail.gennaker': 'Gennaker',
|
|
450
|
+
'sail.code0': 'Code 0',
|
|
451
|
+
'sail.storm_jib': 'Tourmentin'
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
export const LANGUAGES = Object.keys(MESSAGES);
|
|
456
|
+
|
|
457
|
+
// `?lang=` wins, then the browser's preferred languages, then English.
|
|
458
|
+
export function pickLanguage(browserLanguages = [], search = '') {
|
|
459
|
+
const requested = new URLSearchParams(search).get('lang');
|
|
460
|
+
const candidates = [requested, ...browserLanguages].filter(Boolean);
|
|
461
|
+
for (const candidate of candidates) {
|
|
462
|
+
const base = candidate.toLowerCase().split('-')[0];
|
|
463
|
+
if (LANGUAGES.includes(base)) {
|
|
464
|
+
return base;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return 'en';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function createTranslator(language) {
|
|
471
|
+
const messages = MESSAGES[language] ?? MESSAGES.en;
|
|
472
|
+
function t(key, params = {}) {
|
|
473
|
+
const template = messages[key] ?? MESSAGES.en[key] ?? key;
|
|
474
|
+
return template.replace(/\{(\w+)\}/g, (match, name) =>
|
|
475
|
+
params[name] === undefined ? match : String(params[name])
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
t.has = (key) => key in messages || key in MESSAGES.en;
|
|
479
|
+
return t;
|
|
480
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// crypto.randomUUID() exists only in secure contexts, and a boat's Signal K
|
|
2
|
+
// server is usually plain HTTP on the local network; getRandomValues is not
|
|
3
|
+
// restricted.
|
|
4
|
+
export function randomId(crypto = globalThis.crypto) {
|
|
5
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
6
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
7
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
8
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
9
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { html, render } from '../vendor/preact-htm.mjs';
|
|
2
|
+
import { createLocale, LocaleProvider, useLocale, useRoute } from './context.mjs';
|
|
3
|
+
import { pickLanguage } from './i18n.mjs';
|
|
4
|
+
import { ExportView } from './components/ExportView.mjs';
|
|
5
|
+
import { LogView } from './components/LogView.mjs';
|
|
6
|
+
import { PassageView } from './components/PassageView.mjs';
|
|
7
|
+
import { StatusBar } from './components/StatusBar.mjs';
|
|
8
|
+
|
|
9
|
+
function Page({ route }) {
|
|
10
|
+
if (route.name === 'passage') {
|
|
11
|
+
return html`<${PassageView} id=${route.id} />`;
|
|
12
|
+
}
|
|
13
|
+
return route.name === 'export' ? html`<${ExportView} />` : html`<${LogView} />`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Shell() {
|
|
17
|
+
const { t } = useLocale();
|
|
18
|
+
const route = useRoute();
|
|
19
|
+
const current = (name) => (route.name === name ? 'page' : undefined);
|
|
20
|
+
|
|
21
|
+
return html`
|
|
22
|
+
<header class="topbar">
|
|
23
|
+
<a class="brand" href="#/">
|
|
24
|
+
<img src="icon.svg" alt="" width="28" height="28" />
|
|
25
|
+
${t('app.title')}
|
|
26
|
+
</a>
|
|
27
|
+
<nav>
|
|
28
|
+
<a href="#/" aria-current=${route.name === 'export' ? undefined : 'page'}
|
|
29
|
+
>${t('nav.log')}</a
|
|
30
|
+
>
|
|
31
|
+
<a href="#/export" aria-current=${current('export')}>${t('nav.export')}</a>
|
|
32
|
+
<a href="entry/">${t('nav.entry')}</a>
|
|
33
|
+
</nav>
|
|
34
|
+
</header>
|
|
35
|
+
<${StatusBar} />
|
|
36
|
+
<main><${Page} route=${route} /></main>
|
|
37
|
+
<footer class="attribution">${t('footer.attribution')}</footer>
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const locale = createLocale(pickLanguage(navigator.languages, location.search));
|
|
42
|
+
document.documentElement.lang = locale.language;
|
|
43
|
+
document.title = locale.t('app.title');
|
|
44
|
+
|
|
45
|
+
render(
|
|
46
|
+
html`<${LocaleProvider} value=${locale}><${Shell} /></${LocaleProvider}>`,
|
|
47
|
+
document.getElementById('app')
|
|
48
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Why passage detection works from speed alone, in words the crew can act on:
|
|
2
|
+
// the fix for a missing signalk-autostate is not the fix for another source
|
|
3
|
+
// hiding its value.
|
|
4
|
+
export function fallbackMessage(stateIssue, t, format) {
|
|
5
|
+
const source = stateIssue?.source ?? '?';
|
|
6
|
+
switch (stateIssue?.reason) {
|
|
7
|
+
case 'pending':
|
|
8
|
+
return t('status.fallbackPending');
|
|
9
|
+
case 'stale':
|
|
10
|
+
return t('status.fallbackStale', {
|
|
11
|
+
source,
|
|
12
|
+
time: stateIssue.updatedAt
|
|
13
|
+
? `${format.shortDate(stateIssue.updatedAt)} ${format.time(stateIssue.updatedAt)}`
|
|
14
|
+
: '?'
|
|
15
|
+
});
|
|
16
|
+
case 'unrecognised':
|
|
17
|
+
return t('status.fallbackUnrecognised', { source, value: String(stateIssue.value) });
|
|
18
|
+
default:
|
|
19
|
+
return t('status.fallback');
|
|
20
|
+
}
|
|
21
|
+
}
|