mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Simple localization support and currency selector.
Summary: - Grist document has a associated "locale" setting that affects how currency is formatted. - Currency selector for number format. Test Plan: not done Reviewers: dsagal Reviewed By: dsagal Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D2977
This commit is contained in:
@@ -85,6 +85,7 @@ bluebird.promisifyAll(tmp);
|
||||
const MAX_RECENT_ACTIONS = 100;
|
||||
|
||||
const DEFAULT_TIMEZONE = (process.versions as any).electron ? moment.tz.guess() : "UTC";
|
||||
const DEFAULT_LOCALE = "en-US";
|
||||
|
||||
// Number of seconds an ActiveDoc is retained without any clients.
|
||||
// In dev environment, it is convenient to keep this low for quick tests.
|
||||
@@ -338,10 +339,11 @@ export class ActiveDoc extends EventEmitter {
|
||||
await this._docManager.storageManager.prepareToCreateDoc(this.docName);
|
||||
await this.docStorage.createFile();
|
||||
await this._rawPyCall('load_empty');
|
||||
const timezone = docSession.browserSettings ? docSession.browserSettings.timezone : DEFAULT_TIMEZONE;
|
||||
const timezone = docSession.browserSettings?.timezone ?? DEFAULT_TIMEZONE;
|
||||
const locale = docSession.browserSettings?.locale ?? DEFAULT_LOCALE;
|
||||
// This init action is special. It creates schema tables, and is used to init the DB, but does
|
||||
// not go through other steps of a regular action (no ActionHistory or broadcasting).
|
||||
const initBundle = await this._rawPyCall('apply_user_actions', [["InitNewDoc", timezone]]);
|
||||
const initBundle = await this._rawPyCall('apply_user_actions', [["InitNewDoc", timezone, locale]]);
|
||||
await this.docStorage.execTransaction(() =>
|
||||
this.docStorage.applyStoredActions(getEnvContent(initBundle.stored)));
|
||||
|
||||
|
||||
@@ -84,7 +84,8 @@ export class Client {
|
||||
constructor(
|
||||
private _comm: any,
|
||||
private _methods: any,
|
||||
private _host: string
|
||||
private _host: string,
|
||||
private _locale?: string,
|
||||
) {
|
||||
this.clientId = generateClientId();
|
||||
}
|
||||
@@ -102,6 +103,10 @@ export class Client {
|
||||
return this._host;
|
||||
}
|
||||
|
||||
public get locale(): string|undefined {
|
||||
return this._locale;
|
||||
}
|
||||
|
||||
public setConnection(websocket: any, reqHost: string, browserSettings: BrowserSettings) {
|
||||
this._websocket = websocket;
|
||||
// Set this._loginState, used by CognitoClient to construct login/logout URLs.
|
||||
|
||||
@@ -49,6 +49,7 @@ var gutil = require('app/common/gutil');
|
||||
const {parseFirstUrlPart} = require('app/common/gristUrls');
|
||||
const version = require('app/common/version');
|
||||
const {Client} = require('./Client');
|
||||
const {localeFromRequest} = require('app/server/lib/ServerLocale');
|
||||
|
||||
// Bluebird promisification, to be able to use e.g. websocket.sendAsync method.
|
||||
Promise.promisifyAll(ws.prototype);
|
||||
@@ -216,7 +217,7 @@ Comm.prototype._onWebSocketConnection = async function(websocket, req) {
|
||||
}
|
||||
client.setConnection(websocket, req.headers.host, browserSettings);
|
||||
} else {
|
||||
client = new Client(this, this.methods, req.headers.host);
|
||||
client = new Client(this, this.methods, req.headers.host, localeFromRequest(req));
|
||||
client.setCounter(counter);
|
||||
client.setConnection(websocket, req.headers.host, browserSettings);
|
||||
this._clients[client.clientId] = client;
|
||||
|
||||
@@ -32,6 +32,7 @@ import { exportToDrive } from "app/server/lib/GoogleExport";
|
||||
import { googleAuthTokenMiddleware } from "app/server/lib/GoogleAuth";
|
||||
import * as _ from "lodash";
|
||||
import {isRaisedException} from "app/common/gristTypes";
|
||||
import {localeFromRequest} from "app/server/lib/ServerLocale";
|
||||
|
||||
// Cap on the number of requests that can be outstanding on a single document via the
|
||||
// rest doc api. When this limit is exceeded, incoming requests receive an immediate
|
||||
@@ -590,6 +591,7 @@ export class DocWorkerApi {
|
||||
if (parameters.workspaceId) { throw new Error('workspaceId not supported'); }
|
||||
const browserSettings: BrowserSettings = {};
|
||||
if (parameters.timezone) { browserSettings.timezone = parameters.timezone; }
|
||||
browserSettings.locale = localeFromRequest(req);
|
||||
if (uploadId !== undefined) {
|
||||
const result = await this._docManager.importDocToWorkspace(userId, uploadId, null,
|
||||
browserSettings);
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface OptDocSession {
|
||||
|
||||
export function makeOptDocSession(client: Client|null, browserSettings?: BrowserSettings): OptDocSession {
|
||||
if (client && !browserSettings) { browserSettings = client.browserSettings; }
|
||||
if (client && browserSettings && !browserSettings.locale) { browserSettings.locale = client.locale; }
|
||||
return {client, browserSettings};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import {GristType} from 'app/common/gristTypes';
|
||||
import * as gutil from 'app/common/gutil';
|
||||
import * as gristTypes from 'app/common/gristTypes';
|
||||
import {NumberFormatOptions} from 'app/common/NumberFormat';
|
||||
import {formatUnknown, IsRightTypeFunc} from 'app/common/ValueFormatter';
|
||||
import {FormatOptions, formatUnknown, IsRightTypeFunc} from 'app/common/ValueFormatter';
|
||||
import {decodeObject} from 'app/plugin/objtypes';
|
||||
import {Style} from 'exceljs';
|
||||
import * as moment from 'moment-timezone';
|
||||
@@ -17,9 +17,9 @@ interface WidgetOptions extends NumberFormatOptions {
|
||||
}
|
||||
class BaseFormatter {
|
||||
protected isRightType: IsRightTypeFunc;
|
||||
protected widgetOptions: WidgetOptions = {};
|
||||
protected widgetOptions: WidgetOptions;
|
||||
|
||||
constructor(public type: string, public opts: object) {
|
||||
constructor(public type: string, public opts: FormatOptions) {
|
||||
this.isRightType = gristTypes.isRightType(gristTypes.extractTypeFromColType(type)) ||
|
||||
gristTypes.isRightType('Any')!;
|
||||
this.widgetOptions = opts;
|
||||
@@ -164,11 +164,11 @@ const formatters: Partial<Record<GristType, typeof BaseFormatter>> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes column type and widget options and returns a constructor with a format function that can
|
||||
* Takes column type and format options and returns a constructor with a format function that can
|
||||
* properly convert a value passed to it into the right javascript object for that column.
|
||||
* Exceljs library is using javascript primitives to specify correct excel type.
|
||||
*/
|
||||
export function createExcelFormatter(type: string, opts: object): BaseFormatter {
|
||||
export function createExcelFormatter(type: string, opts: FormatOptions): BaseFormatter {
|
||||
const ctor = formatters[gristTypes.extractTypeFromColType(type) as GristType] || AnyFormatter;
|
||||
return new ctor(type, opts);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {buildRowFilter} from 'app/common/RowFilterFunc';
|
||||
import {SchemaTypes} from 'app/common/schema';
|
||||
import {SortFunc} from 'app/common/SortFunc';
|
||||
import {TableData} from 'app/common/TableData';
|
||||
import {DocumentSettings} from 'app/common/DocumentSettings';
|
||||
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
||||
import {RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||
import {docSessionFromRequest} from 'app/server/lib/DocSession';
|
||||
@@ -60,6 +61,10 @@ export interface ExportData {
|
||||
* Columns information (primary used for formatting).
|
||||
*/
|
||||
columns: ExportColumn[];
|
||||
/**
|
||||
* Document settings
|
||||
*/
|
||||
docSettings: DocumentSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,12 +199,15 @@ export async function exportTable(
|
||||
tableName = view.name;
|
||||
}
|
||||
|
||||
const docInfo = safeRecord(safeTable(docData, '_grist_DocInfo'), 1) as DocInfo;
|
||||
const docSettings = gutil.safeJsonParse(docInfo.documentSettings, {});
|
||||
return {
|
||||
tableName,
|
||||
docName: activeDoc.docName,
|
||||
rowIds,
|
||||
access,
|
||||
columns
|
||||
columns,
|
||||
docSettings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -277,11 +285,15 @@ export async function exportSection(
|
||||
// filter rows numbers
|
||||
rowIds = rowIds.filter(rowFilter);
|
||||
|
||||
const docInfo = safeRecord(safeTable(docData, '_grist_DocInfo'), 1) as DocInfo;
|
||||
const docSettings = gutil.safeJsonParse(docInfo.documentSettings, {});
|
||||
|
||||
return {
|
||||
tableName: table.tableId,
|
||||
docName: activeDoc.docName,
|
||||
rowIds,
|
||||
access,
|
||||
docSettings,
|
||||
columns: viewColumns
|
||||
};
|
||||
}
|
||||
@@ -295,6 +307,7 @@ type GristTables = RowModel<'_grist_Tables'>
|
||||
type GristViewsSectionField = RowModel<'_grist_Views_section_field'>
|
||||
type GristTablesColumn = RowModel<'_grist_Tables_column'>
|
||||
type GristView = RowModel<'_grist_Views'>
|
||||
type DocInfo = RowModel<'_grist_DocInfo'>
|
||||
|
||||
// Type for filters passed from the client
|
||||
export interface Filter { colRef: number, filter: string }
|
||||
|
||||
@@ -33,11 +33,12 @@ export async function makeCSV(
|
||||
function convertToCsv({
|
||||
rowIds,
|
||||
access,
|
||||
columns: viewColumns
|
||||
columns: viewColumns,
|
||||
docSettings
|
||||
}: ExportData) {
|
||||
|
||||
// create formatters for columns
|
||||
const formatters = viewColumns.map(col => createFormatter(col.type, col.widgetOptions));
|
||||
const formatters = viewColumns.map(col => createFormatter(col.type, col.widgetOptions, docSettings));
|
||||
// Arrange the data into a row-indexed matrix, starting with column headers.
|
||||
const csvMatrix = [viewColumns.map(col => col.label)];
|
||||
// populate all the rows with values as strings
|
||||
|
||||
16
app/server/lib/ServerLocale.ts
Normal file
16
app/server/lib/ServerLocale.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {parse as languageParser} from "accept-language-parser";
|
||||
import {Request} from 'express';
|
||||
import {locales} from 'app/common/Locales';
|
||||
|
||||
/**
|
||||
* Returns the locale from a request, falling back to `defaultLocale`
|
||||
* if unable to determine the locale.
|
||||
*/
|
||||
export function localeFromRequest(req: Request, defaultLocale: string = 'en-US') {
|
||||
const language = languageParser(req.headers["accept-language"] as string)[0];
|
||||
if (!language) { return defaultLocale; }
|
||||
|
||||
const locale = `${language.code}-${language.region}`;
|
||||
const supports = locales.some(l => l.code === locale);
|
||||
return supports ? locale : defaultLocale;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../buildtools/tsconfig-base.json",
|
||||
"references": [
|
||||
{ "path": "../common" }
|
||||
{ "path": "../common" },
|
||||
],
|
||||
"include": [
|
||||
"**/*",
|
||||
|
||||
Reference in New Issue
Block a user