Skip to content
Snippets Groups Projects
Commit 68739beb authored by Sameer Al-Sakran's avatar Sameer Al-Sakran Committed by GitHub
Browse files

Merge pull request #5803 from metabase/i18n

Internationalization
parents 028506b3 96608e1f
Branches
Tags
No related merge requests found
Showing
with 390 additions and 31 deletions
......@@ -11,6 +11,16 @@
"env": {
"development": {
"presets": []
},
"extract": {
"plugins": [
["c-3po", {
"extract": {
"output": "locales/metabase-frontend.pot"
},
"discover": ["t", "jt"]
}]
]
}
}
}
......@@ -42,7 +42,8 @@
"flowtype/use-flow-type": 1
},
"globals": {
"pending": false
"pending": false,
"t": false
},
"env": {
"browser": true,
......
......@@ -45,3 +45,5 @@ bin/release/aws-eb/metabase-aws-eb.zip
/build.xml
/test-report-*
/crate-*
*.po~
/locales/metabase-*.pot
#!/usr/bin/env node
// This program compiles a ".po" translations file to a JSON version suitable for use on the frontend
// It removes strings that aren't used on the frontend, and other extraneous information like comments
const fs = require("fs");
const _ = require("underscore");
const gParser = require("gettext-parser");
if (process.argv.length !== 4) {
console.log("USAGE: build-translation-frontend-resource input.po output.json");
process.exit(1);
}
const inputFile = process.argv[2];
const outputFile = process.argv[3];
const translationObject = gParser.po.parse(fs.readFileSync(inputFile, "utf-8"));
// NOTE: unsure why the headers are duplicated in a translation for "", but we don't need it
delete translationObject.translations[""][""]
for (const id in translationObject.translations[""]) {
const translation = translationObject.translations[""][id];
if (!translation.comments.reference || _.any(translation.comments.reference.split("\n"), reference => reference.startsWith("frontend/"))) {
// remove comments:
delete translation.comments;
// NOTE: would be nice if we could remove the message id since it's redundant:
// delete translation.msgid;
} else {
// remove strings that aren't in the frontend
delete translationObject.translations[""][id];
}
}
fs.writeFileSync(outputFile, JSON.stringify(translationObject, null, 2), "utf-8");
#!/bin/sh
set -eu
# gettext installed via homebrew is "keg-only", add it to the PATH
if [ -d "/usr/local/opt/gettext/bin" ]; then
export PATH="/usr/local/opt/gettext/bin:$PATH"
fi
POT_NAME="locales/metabase.pot"
LOCALES=$(find locales -type f -name "*.po" -exec basename {} .po \;)
LOCALES_QUOTED=$(echo "$LOCALES" | awk '{ printf "\"%s\" ", $0 }')
FRONTEND_LANG_DIR="resources/frontend_client/app/locales"
# backend
# NOTE: include "en" even though we don't have a .po file for it because it's the default?
cat << EOF > "resources/locales.clj"
{
:locales #{"en" $LOCALES_QUOTED}
:packages ["metabase"]
:bundle "metabase.Messages"
}
EOF
mkdir -p "$FRONTEND_LANG_DIR"
for LOCALE in $LOCALES; do
LOCALE_FILE="locales/$LOCALE.po"
# frontend
# NOTE: just copy these for now, but eventially precompile from .po to .json
./bin/i18n/build-translation-frontend-resource \
"$LOCALE_FILE" \
"$FRONTEND_LANG_DIR/$LOCALE.json"
# backend
msgfmt \
--java2 \
-d "resources" \
-r "metabase.Messages" \
-l "$LOCALE" \
"$LOCALE_FILE"
done
#!/bin/sh
set -eu
# gettext installed via homebrew is "keg-only", add it to the PATH
if [ -d "/usr/local/opt/gettext/bin" ]; then
export PATH="/usr/local/opt/gettext/bin:$PATH"
fi
POT_NAME="locales/metabase.pot"
PO_NAME="locales/$1.po"
if [ $# -lt 1 ]; then
echo "USAGE: update-translation en_US"
exit 1
fi
if [ -f "$PO_NAME" ]; then
exec msgmerge -U "$PO_NAME" "$POT_NAME"
else
exec msginit -i "$POT_NAME" -o "$PO_NAME" -l "$1"
fi
#!/bin/sh
set -eu
# gettext installed via homebrew is "keg-only", add it to the PATH
if [ -d "/usr/local/opt/gettext/bin" ]; then
export PATH="/usr/local/opt/gettext/bin:$PATH"
fi
# check xgettext is installed
if ! command -v xgettext > /dev/null; then
echo 'Please install the "xgettext" command (e.x. `brew install gettext`)'
exit 1
fi
POT_NAME="locales/metabase.pot"
POT_BACKEND_NAME="locales/metabase-backend.pot"
POT_FRONTEND_NAME="locales/metabase-frontend.pot"
mkdir -p "locales"
# update frontend pot
# NOTE: about twice as fast to call babel directly rather than a full webpack build
BABEL_ENV=extract ./node_modules/.bin/babel -q -x .js,.jsx -o /dev/null frontend/src
# BABEL_ENV=extract BABEL_DISABLE_CACHE=1 yarn run build
# update backend pot
# xgettext before 0.19 does not understand --add-location=file. Even CentOS
# 7 ships with an older gettext. We will therefore generate full location
# info on those systems, and only file names where xgettext supports it
LOC_OPT=$(xgettext --add-location=file -f - </dev/null >/dev/null 2>&1 && echo --add-location=file || echo --add-location)
find src -name "*.clj" | xgettext \
--from-code=UTF-8 \
--language=lisp \
--copyright-holder='Metabase <docs@metabase.com>' \
--package-name="metabase" \
--msgid-bugs-address="docs@metabase.com" \
-k \
-kmark:1 -ki18n/mark:1 \
-ktrs:1 -ki18n/trs:1 \
-ktru:1 -ki18n/tru:1 \
-ktrun:1,2 -ki18n/trun:1,2 \
-ktrsn:1,2 -ki18n/trsn:1,2 \
$LOC_OPT \
--add-comments --sort-by-file \
-o $POT_BACKEND_NAME -f -
sed -i "" -e 's/charset=CHARSET/charset=UTF-8/' "$POT_BACKEND_NAME"
# merge frontend and backend pots
msgcat "$POT_FRONTEND_NAME" "$POT_BACKEND_NAME" > "$POT_NAME"
#!/bin/sh
set -eu
./bin/i18n/update-translation-template
find locales -name "*.po" -exec sh -c './bin/i18n/update-translation $(basename {} .po)' \;
import React from "react";
import Select from "metabase/components/Select.jsx";
import _ from "underscore";
const SettingSelect = ({ setting, updateSetting, disabled }) =>
<Select
className="full-width"
placeholder={setting.placeholder}
value={setting.value}
value={_.findWhere(setting.options, { value: setting.value }) || setting.value}
options={setting.options}
onChange={updateSetting}
optionNameFn={option => typeof option === "object" ? option.name : option }
......
......@@ -51,6 +51,14 @@ const SECTIONS = [
note: "Not all databases support timezones, in which case this setting won't take effect.",
allowValueCollection: true
},
{
key: "site-locale",
display_name: "Language",
type: "select",
options: (MetabaseSettings.get("available_locales") || []).map(([value, name]) => ({ name, value })),
placeholder: "Select a language",
getHidden: () => MetabaseSettings.get("available_locales").length < 2
},
{
key: "anon-tracking-enabled",
display_name: "Anonymous Tracking",
......
......@@ -3,6 +3,17 @@
import 'babel-polyfill';
import 'number-to-locale-string';
// make the i18n function "t" global so we don't have to import it in basically every file
import { t, jt } from "c-3po";
global.t = t;
global.jt = jt;
// set the locale before loading anything else
import { setLocalization } from "metabase/lib/i18n";
if (window.MetabaseLocalization) {
setLocalization(window.MetabaseLocalization)
}
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
......
......@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from "prop-types";
import { Link } from "react-router";
import _ from 'underscore';
import { t } from 'c-3po'
import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper.jsx';
import ActivityItem from './ActivityItem.jsx';
......@@ -80,9 +81,7 @@ export default class Activity extends Component {
// this is a base to start with
const description = {
userName: this.userName(item.user, user),
subject: "did some super awesome stuff thats hard to describe",
subjectRefLink: null,
subjectRefName: null,
summary: "did some super awesome stuff thats hard to describe",
timeSince: item.timestamp.fromNow()
};
......@@ -130,8 +129,8 @@ export default class Activity extends Component {
}
break;
case "install":
description.userName = "Hello World!";
description.summary = "Metabase is up and running.";
description.userName = t`Hello World!`;
description.summary = t`Metabase is up and running.`;
break;
case "metric-create":
if(item.model_exists) {
......@@ -175,10 +174,10 @@ export default class Activity extends Component {
}
break;
case "segment-delete":
description.summary = "removed the filter "+item.details.name;
description.summary = t`removed the filter {item.details.name}`;
break;
case "user-joined":
description.summary = "joined!";
description.summary = t`joined!`;
break;
}
......
import React, { Component } from "react";
import { Link } from "react-router";
import { t } from 'c-3po'
import { SetupApi } from "metabase/services";
import SidebarSection from "./SidebarSection.jsx";
......@@ -28,7 +30,9 @@ export default class NextStep extends Component {
const { next } = this.state;
if (next) {
return (
<SidebarSection title="Setup Tip" icon="info" extra={<Link to="/admin/settings" className="text-brand no-decoration">View all</Link>}>
<SidebarSection title={t`Setup Tip`} icon="info" extra={
<Link to="/admin/settings" className="text-brand no-decoration">{t`View all`}</Link>
}>
<Link to={next.link} className="block p3 no-decoration">
<h4 className="text-brand text-bold">{next.title}</h4>
<p className="m0 mt1">{next.description}</p>
......
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Link } from "react-router";
import { t } from 'c-3po'
import Icon from "metabase/components/Icon.jsx";
import SidebarSection from "./SidebarSection.jsx";
......@@ -35,7 +36,7 @@ export default class RecentViews extends Component {
render() {
const { recentViews } = this.props;
return (
<SidebarSection title="Recently Viewed" icon="clock">
<SidebarSection title={t`Recently Viewed`} icon="clock">
{recentViews.length > 0 ?
<ul className="p2">
{recentViews.map((item, index) => {
......@@ -57,7 +58,7 @@ export default class RecentViews extends Component {
:
<div className="flex flex-column layout-centered text-normal text-grey-2">
<p className="p3 text-centered text-grey-2" style={{ "maxWidth": "100%" }}>
You haven't looked at any dashboards or questions recently
{t`You haven't looked at any dashboards or questions recently`}
</p>
</div>
}
......
......@@ -2,6 +2,7 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { t } from 'c-3po'
import Greeting from "metabase/lib/greeting";
import Modal from "metabase/components/Modal.jsx";
......@@ -90,7 +91,7 @@ export default class HomepageApp extends Component {
<div className="flex">
<div className="wrapper">
<div className="Layout-mainColumn pl2">
<div className="pt4 h2 text-normal ml2">Activity</div>
<div className="pt4 h2 text-normal ml2">{t`Activity`}</div>
<Activity {...this.props} />
</div>
</div>
......
import { t } from 'c-3po'
const greetingPrefixes = [
'Hey there,',
'How\'s it going,',
'Howdy,',
'Greetings,',
'Good to see you,',
t`Hey there`,
t`How's it going`,
t`Howdy`,
t`Greetings`,
t`Good to see you`
];
const subheadPrefixes = [
'What do you want to know?',
'What\'s on your mind?',
'What do you want to find out?',
t`What do you want to know?`,
t`What's on your mind?`,
t`What do you want to find out?`
];
var Greeting = {
simpleGreeting: function() {
// TODO - this can result in an undefined thing
......@@ -23,12 +24,12 @@ var Greeting = {
sayHello: function(personalization) {
if(personalization) {
var g = Greeting.simpleGreeting();
if (g === 'How\'s it going,'){
return g + ' ' + personalization + '?';
if (g === t`How's it going`){
return g + ', ' + personalization + '?';
} else {
return g + ' ' + personalization;
return g + ', ' + personalization;
}
} else {
return Greeting.simpleGreeting();
}
......
import { addLocale, useLocale } from "c-3po";
import { I18NApi } from "metabase/services";
export async function loadLocalization(locale) {
// load and parse the locale
const translationsObject = await I18NApi.locale({ locale });
setLocalization(translationsObject);
}
export function setLocalization(translationsObject) {
const locale = translationsObject.headers.language;
// add and set locale with C-3PO
addLocale(locale, translationsObject);
useLocale(locale);
}
import React, { Component } from 'react';
import PropTypes from "prop-types";
import cx from "classnames";
import { t } from 'c-3po'
import { connect } from "react-redux";
import { push } from "react-router-redux";
......@@ -121,20 +122,20 @@ export default class Navbar extends Component {
</Link>
</li>
<li className="pl3 hide xs-show">
<MainNavLink to="/dashboards" name="Dashboards" eventName="Dashboards" />
<MainNavLink to="/dashboards" name={t`Dashboards`} eventName="Dashboards" />
</li>
<li className="pl1 hide xs-show">
<MainNavLink to="/questions" name="Questions" eventName="Questions" />
<MainNavLink to="/questions" name={t`Questions`} eventName="Questions" />
</li>
<li className="pl1 hide sm-show">
<MainNavLink to="/pulse" name="Pulses" eventName="Pulses" />
<MainNavLink to="/pulse" name={t`Pulses`} eventName="Pulses" />
</li>
<li className="pl1 hide sm-show">
<MainNavLink to="/reference/guide" name="Data Reference" eventName="DataReference" />
<MainNavLink to="/reference/guide" name={t`Data Reference`} eventName="DataReference" />
</li>
<li className="pl3 hide sm-show">
<Link to={Urls.newQuestion()} data-metabase-event={"Navbar;New Question"} style={BUTTON_PADDING_STYLES.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all">
New <span>Question</span>
{t`New Question`}
</Link>
</li>
<li className="flex-align-right transition-background">
......
......@@ -277,4 +277,8 @@ export const GeoJSONApi = {
get: GET("/api/geojson/:id"),
};
export const I18NApi = {
locale: GET("/app/locales/:locale.json"),
}
global.services = exports;
# #-#-#-#-# metabase-backend.pot (metabase) #-#-#-#-#
# German translations for metabase package.
# Copyright (C) 2017 Metabase <docs@metabase.com>
# This file is distributed under the same license as the metabase package.
# Tom Robinson <tom@metabase.com>, 2017.
#
msgid ""
msgstr ""
"Project-Id-Version: metabase\n"
"Report-Msgid-Bugs-To: docs@metabase.com\n"
"POT-Creation-Date: 2017-08-21 21:54+0200\n"
"PO-Revision-Date: 2017-08-21 21:17+0200\n"
"Last-Translator: Tom Robinson <tom@metabase.com>\n"
"Language-Team: German <translation-team-de@lists.sourceforge.net>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"#-#-#-#-# metabase-frontend.pot #-#-#-#-#\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"#-#-#-#-# metabase-backend.pot (metabase) #-#-#-#-#\n"
#: frontend/src/metabase/home/components/Activity.jsx:131
msgid "Hello World!"
msgstr "Hallo Welt!"
#: frontend/src/metabase/home/components/Activity.jsx:132
msgid "Metabase is up and running."
msgstr "Metabase ist auf und laufen"
#: frontend/src/metabase/home/components/Activity.jsx:176
msgid "removed the filter {item.details.name}"
msgstr "entfernen sie den filter {item.details.name}"
#: frontend/src/metabase/home/components/Activity.jsx:179
msgid "joined!"
msgstr "beigetreten!"
#: frontend/src/metabase/home/components/NextStep.jsx:31
msgid "Setup Tip"
msgstr "Setup Tipp"
#: frontend/src/metabase/home/components/NextStep.jsx:32
msgid "View all"
msgstr "Alle Anzeigen"
#: frontend/src/metabase/home/components/RecentViews.jsx:38
msgid "Recently Viewed"
msgstr "Vor Kurzem Anzeige"
#: frontend/src/metabase/home/components/RecentViews.jsx:60
msgid "You haven't looked at any dashboards or questions recently"
msgstr "Sie haben nicht schaute zu jeder dashboards oder fragen kürzlich"
#: frontend/src/metabase/home/containers/HomepageApp.jsx:93
msgid "Activity"
msgstr "Aktivitäten"
#: frontend/src/metabase/lib/greeting.js:2
msgid "Hey there"
msgstr "Hallo"
#: frontend/src/metabase/lib/greeting.js:3
#: frontend/src/metabase/lib/greeting.js:25
msgid "How's it going"
msgstr "Wie ist es gehen"
#: frontend/src/metabase/lib/greeting.js:4
msgid "Howdy"
msgstr "Hallo"
#: frontend/src/metabase/lib/greeting.js:5
msgid "Greetings"
msgstr "Grüße"
#: frontend/src/metabase/lib/greeting.js:6
msgid "Good to see you"
msgstr "Schön, dich zu sehen"
#: frontend/src/metabase/lib/greeting.js:10
msgid "What do you want to know?"
msgstr "Was möchten sie wissen?"
#: frontend/src/metabase/lib/greeting.js:11
msgid "What's on your mind?"
msgstr "Was auf dem herzen?"
#: frontend/src/metabase/lib/greeting.js:12
msgid "What do you want to find out?"
msgstr "Was möchten sie finden ot?"
#: frontend/src/metabase/nav/containers/Navbar.jsx:129
msgid "Dashboards"
msgstr "Übersicht"
#: frontend/src/metabase/nav/containers/Navbar.jsx:132
msgid "Questions"
msgstr "Fragen"
#: frontend/src/metabase/nav/containers/Navbar.jsx:135
msgid "Pulses"
msgstr "Impuse"
#: frontend/src/metabase/nav/containers/Navbar.jsx:138
msgid "Data Reference"
msgstr "Daten Referenz"
#: frontend/src/metabase/nav/containers/Navbar.jsx:142
msgid "New Question"
msgstr "Neue Frage"
#: src/metabase/api/setup.clj
msgid "Add a database"
msgstr "Fügen sue eine Übersicht"
#: src/metabase/api/setup.clj
msgid "Get connected"
msgstr "Holen sie verbunden"
#: src/metabase/api/setup.clj
msgid "Connect to your data so your whole team can start to explore."
msgstr "Schließen sie auf ihre daten so ihre ganze team kann beginnen zu erkuden"
#. This is the very first log message that will get printed.
#. It's here because this is one of the very first namespaces that gets loaded, and the first that has access to the logger
#. It shows up a solid 10-15 seconds before the "Starting Metabase in STANDALONE mode" message because so many other namespaces need to get loaded
#: src/metabase/util.clj
msgid "Loading {0}..."
msgstr "Laden {0}..."
#. This is the very first log message that will get printed.
#. It's here because this is one of the very first namespaces that gets loaded, and the first that has access to the logger
#. It shows up a solid 10-15 seconds before the "Starting Metabase in STANDALONE mode" message because so many other namespaces need to get loaded
#: src/metabase/util.clj
msgid "Metabase"
msgstr "Metabaes"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment