Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Fontra UI Strings to a public spreadsheet #1713

Merged
merged 13 commits into from
Oct 13, 2024
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,10 @@ The following list of features is not complete, but gives you a rough idea of wh

- Serverless Fontra
- Peer-to-peer collaboration

## Translations

We are maintaining various language translations of the UI in a spreadsheet. Please contact us if you'd like to contribute (to) a translation.

- [Translation Documentation](https://docs.google.com/spreadsheets/d/1woTU8dZCHJh7yvdk-N1kgQBUj4Sn3SdRsbKgn6ltJQs/edit?gid=1731105247#gid=1731105247)
- [Fontra UI Strings spreadsheet](https://docs.google.com/spreadsheets/d/1woTU8dZCHJh7yvdk-N1kgQBUj4Sn3SdRsbKgn6ltJQs/edit?usp=sharing)
131 changes: 131 additions & 0 deletions scripts/rebuild_languages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import csv
import io
import json
import os
import pathlib
import subprocess
from urllib.request import urlopen


def prettier(path):
subprocess.run(
[
os.fspath(repoDir / "node_modules" / ".bin" / "prettier"),
os.fspath(path),
"--write",
],
check=True,
)


def jsonDump(s):
return json.dumps(s, ensure_ascii=False)


def downloadSheet(url):
print("downloading", url)
response = urlopen(url)
data = response.read()
file = io.StringIO(data.decode("utf-8"))
reader = csv.reader(file)
return list(reader)


languageSpreadsheetURL = (
"https://docs.google.com/"
"spreadsheets/d/1woTU8dZCHJh7yvdk-N1kgQBUj4Sn3SdRsbKgn6ltJQs/"
"export?format=csv&gid=1383907145"
)

rows = downloadSheet(languageSpreadsheetURL)

numHeaders = 5
headers = rows[:numHeaders]
assert headers[0][0] == "Documentation", headers[0][0]
assert headers[1][2] == "English", headers[1][2]
assert headers[2][2] == "English", headers[2][2]
assert headers[3][2] == "en", headers[3][2]

rows = rows[numHeaders:]

startColumn = 2

languages = []
languageStrings = {}

for columnIndex in range(startColumn, len(headers[1])):
languageInEnglish = headers[1][columnIndex]
languageInLanguage = headers[2][columnIndex]
languageCode = headers[3][columnIndex]
languageStatus = headers[4][columnIndex]
assert languageCode
if not languageStatus.strip():
continue

languages.append(
dict(
code=languageCode,
langEn=languageInEnglish,
langLang=languageInLanguage,
status=languageStatus,
)
)

languageStrings[languageCode] = strings = {}

for row in rows:
key = row[1]
if not key.strip():
continue
string = row[columnIndex]
if not string or string == "-":
string = languageStrings["en"].get(key, "!missing!")
strings[key] = string


repoDir = pathlib.Path(__file__).resolve().parent.parent
langDir = repoDir / "src" / "fontra" / "client" / "lang"
assert langDir.is_dir()
localizationJSPath = repoDir / "src" / "fontra" / "client" / "core" / "localization.js"
assert localizationJSPath.is_file()

localizationJSSource = localizationJSPath.read_text(encoding="utf-8")

languagesList = "\n".join(f" {jsonDump(langs)}," for langs in languages)
languagesBlock = f"""// Don't edit this block, see scripts/rebuild_languages.py
export const languages = [
{languagesList}
];
"""

indexStart = localizationJSSource.find(languagesBlock.splitlines()[0])
assert indexStart > 0
indexEnd = localizationJSSource.find("];\n", indexStart)
assert indexEnd > indexStart

localizationJSSource = (
localizationJSSource[:indexStart]
+ languagesBlock
+ localizationJSSource[indexEnd + 3 :]
)

localizationJSPath.write_text(localizationJSSource, encoding="utf-8")
prettier(localizationJSPath)


languageSourceTemplate = """// Don't edit this file: it is generated by scripts/rebuild_languages.py
export const strings = {{
{stringsBlock}
}};
"""

for languageCode, strings in languageStrings.items():
languagePath = langDir / f"{languageCode.strip()}.js"
print("writing", languagePath)
lines = []
for k, v in sorted(strings.items()):
lines.append(f" {jsonDump(k.strip())}: {jsonDump(v)},")
stringsBlock = "\n".join(lines)
languageSource = languageSourceTemplate.format(stringsBlock=stringsBlock)
languagePath.write_text(languageSource, encoding="utf-8")
prettier(languagePath)
14 changes: 10 additions & 4 deletions src/fontra/client/core/localization.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { ObservableController } from "./observable-object.js";
import { fetchJSON } from "./utils.js";

// Don't edit this block, see scripts/rebuild_languages.py
export const languages = [
{ code: "en", langEn: "English", langLang: "English", status: "done" },
{ code: "zh-CN", langEn: "Simplified Chinese", langLang: "简体中文", status: "beta" },
{ code: "nl", langEn: "Dutch", langLang: "Nederlands", status: "wip" },
];

const debugTranslation = false;
let localizationData = {};
Expand All @@ -20,9 +26,9 @@ export const ensureLanguageHasLoaded = new Promise((resolve) => {
function languageChanged(locale) {
// Do explicit .replace() because our cache busting mechanism is simplistic,
// and backtick strings don't work.
const translationsPath = "/lang/locale.json".replace("locale", locale);
fetchJSON(translationsPath).then((data) => {
localizationData = data;
const translationsPath = "/lang/locale.js".replace("locale", locale);
import(translationsPath).then((mod) => {
localizationData = mod.strings;
resolveLanguageHasLoaded();
});
}
Expand Down
19 changes: 12 additions & 7 deletions src/fontra/client/lang/en.json → src/fontra/client/lang/en.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
// Don't edit this file: it is generated by scripts/rebuild_languages.py
export const strings = {
"action-topics.designspace-navigation": "Designspace Navigation",
"action-topics.export-as": "Export as",
"action-topics.glyph-editor-appearance": "Glyph editor appearance",
Expand Down Expand Up @@ -36,6 +37,8 @@
"action.set-contour-start": "Set Start Point",
"action.undo": "Undo",
"application-settings.clipboard.title": "Clipboard",
"application-settings.display-language.status.beta": "beta",
"application-settings.display-language.status.wip": "work in progress",
"application-settings.display-language.title": "Display Language",
"application-settings.editor-behavior.title": "Editor Behavior",
"application-settings.plugins-manager.title": "Plugin Manager",
Expand Down Expand Up @@ -81,9 +84,10 @@
"dialog.add": "Add",
"dialog.cancel": "Cancel",
"dialog.create": "Create",
"dialog.create-new-glyph.body": "Click \"Create\" if you want to create a new glyph named \"%0\"%1.",
"dialog.create-new-glyph.body.2": " for character \"%0\" (%1)",
"dialog.create-new-glyph.title": "Create a new glyph \"%0\"?",
"dialog.create-new-glyph.body":
'Click "Create" if you want to create a new glyph named "%0"%1.',
"dialog.create-new-glyph.body.2": ' for character "%0" (%1)',
"dialog.create-new-glyph.title": 'Create a new glyph "%0"?',
"dialog.glyphs.search": "Search glyphs",
"dialog.replace": "Replace",
"editor.hand-tool": "Hand Tool",
Expand Down Expand Up @@ -155,7 +159,8 @@
"sidebar.reference-font": "Reference Font",
"sidebar.referencefont": "Reference font",
"sidebar.referencefont.customcharacter": "Custom character",
"sidebar.referencefont.info": "Drop one or more .ttf, .otf, .woff or .woff2 files in the field below",
"sidebar.referencefont.info":
"Drop one or more .ttf, .otf, .woff or .woff2 files in the field below",
"sidebar.referencefont.language": "Language",
"sidebar.related-glyphs": "Related Glyphs & Characters",
"sidebar.related-glyphs.title": "Related Glyphs & Characters for %0",
Expand Down Expand Up @@ -239,5 +244,5 @@
"toggle-fullscreen": "Toggle Fullscreen",
"zoom-fit-selection": "Zoom To Fit Selection",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out"
}
"zoom-out": "Zoom Out",
};
Loading