Every official language, in JSON: 113 listed, five shipped so far
Build / Desktop, server and tools (push) Successful in 33s
Build / Desktop, server and tools (push) Successful in 33s
The language list comes from CLDR (official in a UN member state, plus the six UN languages), with autonyms and text direction supplied for the 21 that ICU does not know. Strings move from resx to one JSON file per language, looked up by code, so a language unknown to ICU or MSBuild still loads; tools/strings.cs checks keys and placeholders in CI. Arabic, Hindi and Chinese drafts prove right-to-left and system-font fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
7589121846
commit
8eaf9f36fc
17 files changed
+1072
-99
No files matched your search
@@ -13,6 +13,9 @@ dotnet run tools/setup.cs -- --group packaging # pinned nfpm into to
|
|||||||
dotnet build src/MeWeb.Android/MeWeb.Android.csproj \
|
dotnet build src/MeWeb.Android/MeWeb.Android.csproj \
|
||||||
-p:JavaSdkDirectory=$PWD/tools/jdk/linux-x64/jdk-21.0.12+8 -p:AndroidSdkDirectory=$HOME/Android/Sdk
|
-p:JavaSdkDirectory=$PWD/tools/jdk/linux-x64/jdk-21.0.12+8 -p:AndroidSdkDirectory=$HOME/Android/Sdk
|
||||||
dotnet run --project tools/Screenshots -- dist/screenshots [en,it,...]
|
dotnet run --project tools/Screenshots -- dist/screenshots [en,it,...]
|
||||||
|
dotnet run tools/strings.cs -- check # every language against English
|
||||||
|
dotnet run tools/strings.cs -- set Key values.json # one key, every language at once
|
||||||
|
dotnet run tools/languages.cs # regenerate data/languages.json from CLDR
|
||||||
dotnet publish src/MeWeb.Desktop -c Release -r win-x64 --self-contained -p:PublishSingleFile=true \
|
dotnet publish src/MeWeb.Desktop -c Release -r win-x64 --self-contained -p:PublishSingleFile=true \
|
||||||
-p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true
|
-p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true
|
||||||
dotnet publish src/MeWeb.Desktop -c Release -r osx-arm64 --self-contained
|
dotnet publish src/MeWeb.Desktop -c Release -r osx-arm64 --self-contained
|
||||||
@@ -26,11 +29,11 @@ is root-owned, so CI uses the runner user's own `~/.dotnet` (passolo's `tools/re
|
|||||||
| Path | What |
|
| Path | What |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/MeWeb.Shared` | `Endpoints.cs` (routes and hosts), wire contracts, `MeWebJsonContext`, generated `BuildInfo` |
|
| `src/MeWeb.Shared` | `Endpoints.cs` (routes and hosts), wire contracts, `MeWebJsonContext`, generated `BuildInfo` |
|
||||||
| `src/MeWeb.App` | Avalonia views and view models, `Services/Localizer.cs` (+ `{s:Tr Key}`), `Strings/*.resx`, `Resources/Colors.axaml` |
|
| `src/MeWeb.App` | Avalonia views and view models, `Services/Localizer.cs` (+ `{s:Tr Key}`), `Strings/<code>.json`, `Resources/Colors.axaml` |
|
||||||
| `src/MeWeb.Desktop` | desktop head (`meweb` executable), Windows manifest |
|
| `src/MeWeb.Desktop` | desktop head (`meweb` executable), Windows manifest |
|
||||||
| `src/MeWeb.Android` | Android head (`software.arasaka.meweb`), resources, manifest |
|
| `src/MeWeb.Android` | Android head (`software.arasaka.meweb`), resources, manifest |
|
||||||
| `src/MeWeb.Server` | `meweb.arasaka.software`: `/api/info`, `/api/ip` |
|
| `src/MeWeb.Server` | `meweb.arasaka.software`: `/api/info`, `/api/ip` |
|
||||||
| `data/languages.json` | every language with `reviewed` or `draft` |
|
| `data/languages.json` | 113 languages: code, autonym, English name, `rtl`, `reviewed` or `draft` |
|
||||||
| `tools/setup.cs` + `setup.json` | pinned build tools |
|
| `tools/setup.cs` + `setup.json` | pinned build tools |
|
||||||
| `tools/Screenshots` | headless renders of every view in every language |
|
| `tools/Screenshots` | headless renders of every view in every language |
|
||||||
| `branding/` | the icon (SVG source, PNG, ICO) |
|
| `branding/` | the icon (SVG source, PNG, ICO) |
|
||||||
|
|||||||
+10
-4
@@ -131,8 +131,11 @@ Scope: `src/`, `tools/`. Out of scope: generated files (`obj/`, `*.g.cs`), third
|
|||||||
|
|
||||||
38. **Structured logging with `{Placeholder}` templates**, never interpolated strings (Microsoft.Extensions.Logging,
|
38. **Structured logging with `{Placeholder}` templates**, never interpolated strings (Microsoft.Extensions.Logging,
|
||||||
as passolo).
|
as passolo).
|
||||||
39. **Localization-first:** every user-facing string is a resx key in `src/MeWeb.App/Strings/Strings*.resx`; no
|
39. **Localization-first:** every user-facing string is a key in `src/MeWeb.App/Strings/<code>.json`, one flat
|
||||||
literals in XAML or view models (the product name `me-web?` excepted).
|
file per language, English the fallback; no literals in XAML or view models (the product name `me-web?`
|
||||||
|
excepted). JSON rather than resx on purpose: 21 of the 113 official languages are unknown to ICU and to MSBuild's
|
||||||
|
satellite-culture list, and a file looked up by code needs neither. Keys change only through
|
||||||
|
`tools/strings.cs set`, and `tools/strings.cs check` (keys and `{n}` placeholders against English) runs in CI.
|
||||||
40. **Central Package Management** (`Directory.Packages.props`); Avalonia packages move together through
|
40. **Central Package Management** (`Directory.Packages.props`); Avalonia packages move together through
|
||||||
`AvaloniaVersion`.
|
`AvaloniaVersion`.
|
||||||
41. **Docs as convention:** `AGENT.md` (operational), `CLAUDE.md` (quick reference + pitfalls), `PATTERNS.md`
|
41. **Docs as convention:** `AGENT.md` (operational), `CLAUDE.md` (quick reference + pitfalls), `PATTERNS.md`
|
||||||
@@ -172,7 +175,10 @@ Scope: `src/`, `tools/`. Out of scope: generated files (`obj/`, `*.g.cs`), third
|
|||||||
private, loopback, link-local, CGNAT, ULA and the server's own prefixes.
|
private, loopback, link-local, CGNAT, ULA and the server's own prefixes.
|
||||||
124. **Shared findings leave only after the person has read exactly what is sent**, and every submission can be
|
124. **Shared findings leave only after the person has read exactly what is sent**, and every submission can be
|
||||||
deleted with the id and token the app keeps.
|
deleted with the id and token the app keeps.
|
||||||
125. **Every language ships; drafts say so.** `data/languages.json` marks each language `reviewed` or `draft`; a
|
125. **Every language ships; drafts say so.** `data/languages.json` (written by `tools/languages.cs` from CLDR:
|
||||||
draft shows its notice and the "suggest a correction" link. The root `FlowDirection` follows the language.
|
every language official or de facto official in a UN member state, plus the six UN languages) marks each
|
||||||
|
`reviewed` or `draft` and carries its own `rtl` flag, because ICU does not know every script's direction. The
|
||||||
|
picker lists a language once its strings file exists; a draft shows its notice and the "suggest a correction"
|
||||||
|
link; the root `FlowDirection` follows `rtl`.
|
||||||
126. **`?` appears only in display text.** File names, package ids and paths use `MeWeb`, `meweb` and
|
126. **`?` appears only in display text.** File names, package ids and paths use `MeWeb`, `meweb` and
|
||||||
`software.arasaka.meweb`.
|
`software.arasaka.meweb`.
|
||||||
+791
-2
@@ -1,4 +1,793 @@
|
|||||||
[
|
[
|
||||||
{ "code": "en", "name": "English", "status": "reviewed" },
|
{
|
||||||
{ "code": "it", "name": "Italiano", "status": "reviewed" }
|
"code": "am",
|
||||||
|
"name": "አማርኛ",
|
||||||
|
"english": "Amharic",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ar",
|
||||||
|
"name": "العربية",
|
||||||
|
"english": "Arabic",
|
||||||
|
"rtl": true,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ay",
|
||||||
|
"name": "Aymar aru",
|
||||||
|
"english": "Aymara",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "az",
|
||||||
|
"name": "Azərbaycan",
|
||||||
|
"english": "Azerbaijani",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "az-Cyrl",
|
||||||
|
"name": "Азәрбајҹан (Кирил)",
|
||||||
|
"english": "Azerbaijani (Cyrillic)",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "be",
|
||||||
|
"name": "Беларуская",
|
||||||
|
"english": "Belarusian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bg",
|
||||||
|
"name": "Български",
|
||||||
|
"english": "Bulgarian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bi",
|
||||||
|
"name": "Bislama",
|
||||||
|
"english": "Bislama",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bn",
|
||||||
|
"name": "বাংলা",
|
||||||
|
"english": "Bangla",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bs",
|
||||||
|
"name": "Bosanski",
|
||||||
|
"english": "Bosnian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bs-Cyrl",
|
||||||
|
"name": "Босански (ћирилица)",
|
||||||
|
"english": "Bosnian (Cyrillic)",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ca",
|
||||||
|
"name": "Català",
|
||||||
|
"english": "Catalan",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "cs",
|
||||||
|
"name": "Čeština",
|
||||||
|
"english": "Czech",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "da",
|
||||||
|
"name": "Dansk",
|
||||||
|
"english": "Danish",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "de",
|
||||||
|
"name": "Deutsch",
|
||||||
|
"english": "German",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "dv",
|
||||||
|
"name": "ދިވެހި",
|
||||||
|
"english": "Dhivehi",
|
||||||
|
"rtl": true,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "dz",
|
||||||
|
"name": "རྫོང་ཁ",
|
||||||
|
"english": "Dzongkha",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "el",
|
||||||
|
"name": "Ελληνικά",
|
||||||
|
"english": "Greek",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "en",
|
||||||
|
"name": "English",
|
||||||
|
"english": "English",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "reviewed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "es",
|
||||||
|
"name": "Español",
|
||||||
|
"english": "Spanish",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "et",
|
||||||
|
"name": "Eesti",
|
||||||
|
"english": "Estonian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fa",
|
||||||
|
"name": "فارسی",
|
||||||
|
"english": "Persian",
|
||||||
|
"rtl": true,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fi",
|
||||||
|
"name": "Suomi",
|
||||||
|
"english": "Finnish",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fil",
|
||||||
|
"name": "Filipino",
|
||||||
|
"english": "Filipino",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fj",
|
||||||
|
"name": "Na Vosa Vakaviti",
|
||||||
|
"english": "Fijian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fr",
|
||||||
|
"name": "Français",
|
||||||
|
"english": "French",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ga",
|
||||||
|
"name": "Gaeilge",
|
||||||
|
"english": "Irish",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gil",
|
||||||
|
"name": "Taetae ni Kiribati",
|
||||||
|
"english": "Gilbertese",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gn",
|
||||||
|
"name": "Avañe'ẽ",
|
||||||
|
"english": "Guarani",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gsw",
|
||||||
|
"name": "Schwiizertüütsch",
|
||||||
|
"english": "Swiss German",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "he",
|
||||||
|
"name": "עברית",
|
||||||
|
"english": "Hebrew",
|
||||||
|
"rtl": true,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hi",
|
||||||
|
"name": "हिन्दी",
|
||||||
|
"english": "Hindi",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hif",
|
||||||
|
"name": "Fiji Baat",
|
||||||
|
"english": "Fiji Hindi",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ho",
|
||||||
|
"name": "Hiri Motu",
|
||||||
|
"english": "Hiri Motu",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hr",
|
||||||
|
"name": "Hrvatski",
|
||||||
|
"english": "Croatian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ht",
|
||||||
|
"name": "Kreyòl ayisyen",
|
||||||
|
"english": "Haitian Creole",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hu",
|
||||||
|
"name": "Magyar",
|
||||||
|
"english": "Hungarian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hy",
|
||||||
|
"name": "Հայերեն",
|
||||||
|
"english": "Armenian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id",
|
||||||
|
"name": "Indonesia",
|
||||||
|
"english": "Indonesian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "is",
|
||||||
|
"name": "Íslenska",
|
||||||
|
"english": "Icelandic",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "it",
|
||||||
|
"name": "Italiano",
|
||||||
|
"english": "Italian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "reviewed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ja",
|
||||||
|
"name": "日本語",
|
||||||
|
"english": "Japanese",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ka",
|
||||||
|
"name": "ქართული",
|
||||||
|
"english": "Georgian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "kk",
|
||||||
|
"name": "Қазақ тілі",
|
||||||
|
"english": "Kazakh",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "km",
|
||||||
|
"name": "ខ្មែរ",
|
||||||
|
"english": "Khmer",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ko",
|
||||||
|
"name": "한국어",
|
||||||
|
"english": "Korean",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ky",
|
||||||
|
"name": "Кыргызча",
|
||||||
|
"english": "Kyrgyz",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lb",
|
||||||
|
"name": "Lëtzebuergesch",
|
||||||
|
"english": "Luxembourgish",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lo",
|
||||||
|
"name": "ລາວ",
|
||||||
|
"english": "Lao",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lt",
|
||||||
|
"name": "Lietuvių",
|
||||||
|
"english": "Lithuanian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lv",
|
||||||
|
"name": "Latviešu",
|
||||||
|
"english": "Latvian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mg",
|
||||||
|
"name": "Malagasy",
|
||||||
|
"english": "Malagasy",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mh",
|
||||||
|
"name": "Kajin M̧ajeļ",
|
||||||
|
"english": "Marshallese",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mi",
|
||||||
|
"name": "Māori",
|
||||||
|
"english": "Māori",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mk",
|
||||||
|
"name": "Македонски",
|
||||||
|
"english": "Macedonian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mn",
|
||||||
|
"name": "Монгол",
|
||||||
|
"english": "Mongolian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ms",
|
||||||
|
"name": "Melayu",
|
||||||
|
"english": "Malay",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ms-Arab",
|
||||||
|
"name": "بهاس ملايو",
|
||||||
|
"english": "Malay (Arabic script)",
|
||||||
|
"rtl": true,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mt",
|
||||||
|
"name": "Malti",
|
||||||
|
"english": "Maltese",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "my",
|
||||||
|
"name": "မြန်မာ",
|
||||||
|
"english": "Burmese",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "na",
|
||||||
|
"name": "Dorerin Naoero",
|
||||||
|
"english": "Nauruan",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nb",
|
||||||
|
"name": "Norsk bokmål",
|
||||||
|
"english": "Norwegian Bokmål",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nd",
|
||||||
|
"name": "IsiNdebele",
|
||||||
|
"english": "North Ndebele",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ne",
|
||||||
|
"name": "नेपाली",
|
||||||
|
"english": "Nepali",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nl",
|
||||||
|
"name": "Nederlands",
|
||||||
|
"english": "Dutch",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nn",
|
||||||
|
"name": "Norsk nynorsk",
|
||||||
|
"english": "Norwegian Nynorsk",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "no",
|
||||||
|
"name": "Norsk",
|
||||||
|
"english": "Norwegian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ny",
|
||||||
|
"name": "Chichewa",
|
||||||
|
"english": "Chichewa",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pau",
|
||||||
|
"name": "Tekoi ra Belau",
|
||||||
|
"english": "Palauan",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pl",
|
||||||
|
"name": "Polski",
|
||||||
|
"english": "Polish",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ps",
|
||||||
|
"name": "پښتو",
|
||||||
|
"english": "Pashto",
|
||||||
|
"rtl": true,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pt",
|
||||||
|
"name": "Português",
|
||||||
|
"english": "Portuguese",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "qu",
|
||||||
|
"name": "Runasimi",
|
||||||
|
"english": "Quechua",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "rn",
|
||||||
|
"name": "Ikirundi",
|
||||||
|
"english": "Rundi",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ro",
|
||||||
|
"name": "Română",
|
||||||
|
"english": "Romanian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ru",
|
||||||
|
"name": "Русский",
|
||||||
|
"english": "Russian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "rw",
|
||||||
|
"name": "Ikinyarwanda",
|
||||||
|
"english": "Kinyarwanda",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sg",
|
||||||
|
"name": "Sängö",
|
||||||
|
"english": "Sango",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "si",
|
||||||
|
"name": "සිංහල",
|
||||||
|
"english": "Sinhala",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sk",
|
||||||
|
"name": "Slovenčina",
|
||||||
|
"english": "Slovak",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sl",
|
||||||
|
"name": "Slovenščina",
|
||||||
|
"english": "Slovenian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sm",
|
||||||
|
"name": "Gagana Samoa",
|
||||||
|
"english": "Samoan",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sn",
|
||||||
|
"name": "ChiShona",
|
||||||
|
"english": "Shona",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "so",
|
||||||
|
"name": "Soomaali",
|
||||||
|
"english": "Somali",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sq",
|
||||||
|
"name": "Shqip",
|
||||||
|
"english": "Albanian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sr",
|
||||||
|
"name": "Српски",
|
||||||
|
"english": "Serbian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sr-Latn",
|
||||||
|
"name": "Srpski (latinica)",
|
||||||
|
"english": "Serbian (Latin)",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ss",
|
||||||
|
"name": "SiSwati",
|
||||||
|
"english": "Swati",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "st",
|
||||||
|
"name": "Sesotho",
|
||||||
|
"english": "Southern Sotho",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sv",
|
||||||
|
"name": "Svenska",
|
||||||
|
"english": "Swedish",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sw",
|
||||||
|
"name": "Kiswahili",
|
||||||
|
"english": "Swahili",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ta",
|
||||||
|
"name": "தமிழ்",
|
||||||
|
"english": "Tamil",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tet",
|
||||||
|
"name": "Tetun",
|
||||||
|
"english": "Tetum",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tg",
|
||||||
|
"name": "Тоҷикӣ",
|
||||||
|
"english": "Tajik",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "th",
|
||||||
|
"name": "ไทย",
|
||||||
|
"english": "Thai",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ti",
|
||||||
|
"name": "ትግርኛ",
|
||||||
|
"english": "Tigrinya",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tk",
|
||||||
|
"name": "Türkmen dili",
|
||||||
|
"english": "Turkmen",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tn",
|
||||||
|
"name": "Setswana",
|
||||||
|
"english": "Tswana",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "to",
|
||||||
|
"name": "Lea fakatonga",
|
||||||
|
"english": "Tongan",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tpi",
|
||||||
|
"name": "Tok Pisin",
|
||||||
|
"english": "Tok Pisin",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tr",
|
||||||
|
"name": "Türkçe",
|
||||||
|
"english": "Turkish",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tvl",
|
||||||
|
"name": "Te Ggana Tuuvalu",
|
||||||
|
"english": "Tuvaluan",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tzm",
|
||||||
|
"name": "Tamaziɣt n laṭlaṣ",
|
||||||
|
"english": "Central Atlas Tamazight",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "uk",
|
||||||
|
"name": "Українська",
|
||||||
|
"english": "Ukrainian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ur",
|
||||||
|
"name": "اردو",
|
||||||
|
"english": "Urdu",
|
||||||
|
"rtl": true,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "uz",
|
||||||
|
"name": "O‘zbek",
|
||||||
|
"english": "Uzbek",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "uz-Cyrl",
|
||||||
|
"name": "Ўзбекча (Кирил)",
|
||||||
|
"english": "Uzbek (Cyrillic)",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "vi",
|
||||||
|
"name": "Tiếng Việt",
|
||||||
|
"english": "Vietnamese",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "wni",
|
||||||
|
"name": "Shindzuani",
|
||||||
|
"english": "Ndzwani Comorian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "wo",
|
||||||
|
"name": "Wolof",
|
||||||
|
"english": "Wolof",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "yo",
|
||||||
|
"name": "Èdè Yorùbá",
|
||||||
|
"english": "Yoruba",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zdj",
|
||||||
|
"name": "Shingazidja",
|
||||||
|
"english": "Ngazidja Comorian",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zh",
|
||||||
|
"name": "中文",
|
||||||
|
"english": "Chinese",
|
||||||
|
"rtl": false,
|
||||||
|
"status": "draft"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
<EmbeddedResource Include="..\..\data\languages.json" LogicalName="languages.json" />
|
<EmbeddedResource Include="..\..\data\languages.json" LogicalName="languages.json" />
|
||||||
|
<EmbeddedResource Include="Strings\*.json" LogicalName="strings.%(Filename).json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace MeWeb.App.Services;
|
|||||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, WriteIndented = true)]
|
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, WriteIndented = true)]
|
||||||
[JsonSerializable(typeof(List<Language>))]
|
[JsonSerializable(typeof(List<Language>))]
|
||||||
[JsonSerializable(typeof(StoredSettings))]
|
[JsonSerializable(typeof(StoredSettings))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||||
public partial class AppJsonContext : JsonSerializerContext
|
public partial class AppJsonContext : JsonSerializerContext
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ public class Language
|
|||||||
{
|
{
|
||||||
public string Code { get; set; }
|
public string Code { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public string English { get; set; }
|
||||||
|
public bool Rtl { get; set; }
|
||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
|
|
||||||
public bool IsDraft => Status == "draft";
|
public bool IsDraft => Status == "draft";
|
||||||
@@ -14,7 +16,7 @@ public class Language
|
|||||||
|
|
||||||
public static class Languages
|
public static class Languages
|
||||||
{
|
{
|
||||||
public static Language Fallback { get; } = new() { Code = "en", Name = "English", Status = "reviewed" };
|
public static Language Fallback { get; } = new() { Code = "en", Name = "English", English = "English", Status = "reviewed" };
|
||||||
|
|
||||||
public static IReadOnlyList<Language> All { get; } = Load();
|
public static IReadOnlyList<Language> All { get; } = Load();
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ public static class Languages
|
|||||||
using var stream = typeof(Languages).Assembly.GetManifestResourceStream("languages.json");
|
using var stream = typeof(Languages).Assembly.GetManifestResourceStream("languages.json");
|
||||||
if (stream is null) return [Fallback];
|
if (stream is null) return [Fallback];
|
||||||
var list = JsonSerializer.Deserialize(stream, AppJsonContext.Default.ListLanguage) ?? [];
|
var list = JsonSerializer.Deserialize(stream, AppJsonContext.Default.ListLanguage) ?? [];
|
||||||
return list.Count == 0 ? [Fallback] : list.OrderBy(l => l.Name, StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.IgnoreCase)).ToList();
|
var shipped = list.Where(l => Localizer.Has(l.Code)).ToList();
|
||||||
|
return shipped.Count == 0 ? [Fallback] : shipped.OrderBy(l => l.Name, StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.IgnoreCase)).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Resources;
|
using System.Text.Json;
|
||||||
|
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
@@ -10,7 +10,10 @@ namespace MeWeb.App.Services;
|
|||||||
|
|
||||||
public sealed class Localizer : INotifyPropertyChanged
|
public sealed class Localizer : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
readonly ResourceManager resources = new("MeWeb.App.Strings.Strings", typeof(Localizer).Assembly);
|
readonly Dictionary<string, string> english = Load(Languages.Fallback.Code);
|
||||||
|
Dictionary<string, string> current;
|
||||||
|
|
||||||
|
public Localizer() => current = english;
|
||||||
|
|
||||||
public static Localizer Instance { get; } = new();
|
public static Localizer Instance { get; } = new();
|
||||||
|
|
||||||
@@ -20,21 +23,45 @@ public sealed class Localizer : INotifyPropertyChanged
|
|||||||
|
|
||||||
public CultureInfo Culture { get; private set; } = CultureInfo.GetCultureInfo(Languages.Fallback.Code);
|
public CultureInfo Culture { get; private set; } = CultureInfo.GetCultureInfo(Languages.Fallback.Code);
|
||||||
|
|
||||||
public bool IsRightToLeft => Culture.TextInfo.IsRightToLeft;
|
public bool IsRightToLeft => Language.Rtl;
|
||||||
|
|
||||||
public string this[string key] => resources.GetString(key, Culture) ?? key;
|
public string this[string key] => current.GetValueOrDefault(key) ?? english.GetValueOrDefault(key) ?? key;
|
||||||
|
|
||||||
public string Format(string key, params object[] args) => string.Format(Culture, this[key], args);
|
public string Format(string key, params object[] args) => string.Format(Culture, this[key], args);
|
||||||
|
|
||||||
public void SetLanguage(Language language)
|
public void SetLanguage(Language language)
|
||||||
{
|
{
|
||||||
Language = language ?? Languages.Fallback;
|
Language = language ?? Languages.Fallback;
|
||||||
Culture = CultureInfo.GetCultureInfo(Language.Code);
|
current = Language.Code == Languages.Fallback.Code ? english : Load(Language.Code);
|
||||||
|
Culture = CultureFor(Language.Code);
|
||||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = Culture;
|
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = Culture;
|
||||||
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = Culture;
|
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = Culture;
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool Has(string code) => typeof(Localizer).Assembly.GetManifestResourceInfo(ResourceName(code)) is not null;
|
||||||
|
|
||||||
|
static string ResourceName(string code) => $"strings.{code}.json";
|
||||||
|
|
||||||
|
static Dictionary<string, string> Load(string code)
|
||||||
|
{
|
||||||
|
using var stream = typeof(Localizer).Assembly.GetManifestResourceStream(ResourceName(code));
|
||||||
|
if (stream is null) return [];
|
||||||
|
return JsonSerializer.Deserialize(stream, AppJsonContext.Default.DictionaryStringString) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static CultureInfo CultureFor(string code)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return CultureInfo.GetCultureInfo(code);
|
||||||
|
}
|
||||||
|
catch (CultureNotFoundException)
|
||||||
|
{
|
||||||
|
return CultureInfo.InvariantCulture;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TrExtension : MarkupExtension
|
public sealed class TrExtension : MarkupExtension
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<data name="AppTagline" xml:space="preserve">
|
|
||||||
<value>Scopra se da questa connessione può ospitare un server suo, e che cosa lo impedisce.</value>
|
|
||||||
</data>
|
|
||||||
<data name="Check" xml:space="preserve">
|
|
||||||
<value>Verifica</value>
|
|
||||||
</data>
|
|
||||||
<data name="DraftNotice" xml:space="preserve">
|
|
||||||
<value>Questa traduzione è una bozza automatica.</value>
|
|
||||||
</data>
|
|
||||||
<data name="Language" xml:space="preserve">
|
|
||||||
<value>Lingua</value>
|
|
||||||
</data>
|
|
||||||
<data name="StatusChecking" xml:space="preserve">
|
|
||||||
<value>Verifico la connessione…</value>
|
|
||||||
</data>
|
|
||||||
<data name="StatusOffline" xml:space="preserve">
|
|
||||||
<value>Non connesso a internet</value>
|
|
||||||
</data>
|
|
||||||
<data name="StatusOnline" xml:space="preserve">
|
|
||||||
<value>Connesso a internet</value>
|
|
||||||
</data>
|
|
||||||
<data name="SuggestCorrection" xml:space="preserve">
|
|
||||||
<value>Proponi una correzione</value>
|
|
||||||
</data>
|
|
||||||
<data name="VersionLine" xml:space="preserve">
|
|
||||||
<value>me-web? {0} · software libero (AGPL-3.0)</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<data name="AppTagline" xml:space="preserve">
|
|
||||||
<value>Find out whether you can run your own server from this connection, and what stands in the way.</value>
|
|
||||||
</data>
|
|
||||||
<data name="Check" xml:space="preserve">
|
|
||||||
<value>Check</value>
|
|
||||||
</data>
|
|
||||||
<data name="DraftNotice" xml:space="preserve">
|
|
||||||
<value>This translation is a machine draft.</value>
|
|
||||||
</data>
|
|
||||||
<data name="Language" xml:space="preserve">
|
|
||||||
<value>Language</value>
|
|
||||||
</data>
|
|
||||||
<data name="StatusChecking" xml:space="preserve">
|
|
||||||
<value>Checking the connection…</value>
|
|
||||||
</data>
|
|
||||||
<data name="StatusOffline" xml:space="preserve">
|
|
||||||
<value>Not connected to the internet</value>
|
|
||||||
</data>
|
|
||||||
<data name="StatusOnline" xml:space="preserve">
|
|
||||||
<value>Connected to the internet</value>
|
|
||||||
</data>
|
|
||||||
<data name="SuggestCorrection" xml:space="preserve">
|
|
||||||
<value>Suggest a correction</value>
|
|
||||||
</data>
|
|
||||||
<data name="VersionLine" xml:space="preserve">
|
|
||||||
<value>me-web? {0} · free software (AGPL-3.0)</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"AppTagline": "اكتشف ما إذا كان بإمكانك تشغيل خادمك الخاص من هذا الاتصال، وما الذي يعيق ذلك.",
|
||||||
|
"Check": "افحص",
|
||||||
|
"DraftNotice": "هذه الترجمة مسودة آلية.",
|
||||||
|
"Language": "اللغة",
|
||||||
|
"StatusChecking": "جارٍ فحص الاتصال…",
|
||||||
|
"StatusOffline": "غير متصل بالإنترنت",
|
||||||
|
"StatusOnline": "متصل بالإنترنت",
|
||||||
|
"SuggestCorrection": "اقترح تصحيحًا",
|
||||||
|
"VersionLine": "me-web? {0} · برنامج حر (AGPL-3.0)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"AppTagline": "Find out whether you can run your own server from this connection, and what stands in the way.",
|
||||||
|
"Check": "Check",
|
||||||
|
"DraftNotice": "This translation is a machine draft.",
|
||||||
|
"Language": "Language",
|
||||||
|
"StatusChecking": "Checking the connection…",
|
||||||
|
"StatusOffline": "Not connected to the internet",
|
||||||
|
"StatusOnline": "Connected to the internet",
|
||||||
|
"SuggestCorrection": "Suggest a correction",
|
||||||
|
"VersionLine": "me-web? {0} · free software (AGPL-3.0)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"AppTagline": "जानें कि क्या आप इस कनेक्शन से अपना सर्वर चला सकते हैं, और रास्ते में क्या बाधा है।",
|
||||||
|
"Check": "जाँचें",
|
||||||
|
"DraftNotice": "यह अनुवाद एक मशीनी मसौदा है।",
|
||||||
|
"Language": "भाषा",
|
||||||
|
"StatusChecking": "कनेक्शन की जाँच हो रही है…",
|
||||||
|
"StatusOffline": "इंटरनेट से कनेक्ट नहीं है",
|
||||||
|
"StatusOnline": "इंटरनेट से कनेक्ट है",
|
||||||
|
"SuggestCorrection": "सुधार सुझाएँ",
|
||||||
|
"VersionLine": "me-web? {0} · मुक्त सॉफ़्टवेयर (AGPL-3.0)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"AppTagline": "Scopra se da questa connessione può ospitare un server suo, e che cosa lo impedisce.",
|
||||||
|
"Check": "Verifica",
|
||||||
|
"DraftNotice": "Questa traduzione è una bozza automatica.",
|
||||||
|
"Language": "Lingua",
|
||||||
|
"StatusChecking": "Verifico la connessione…",
|
||||||
|
"StatusOffline": "Non connesso a internet",
|
||||||
|
"StatusOnline": "Connesso a internet",
|
||||||
|
"SuggestCorrection": "Proponi una correzione",
|
||||||
|
"VersionLine": "me-web? {0} · software libero (AGPL-3.0)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"AppTagline": "了解您能否通过此网络连接运行自己的服务器,以及有哪些障碍。",
|
||||||
|
"Check": "检测",
|
||||||
|
"DraftNotice": "此翻译为机器草稿。",
|
||||||
|
"Language": "语言",
|
||||||
|
"StatusChecking": "正在检查连接…",
|
||||||
|
"StatusOffline": "未连接到互联网",
|
||||||
|
"StatusOnline": "已连接到互联网",
|
||||||
|
"SuggestCorrection": "建议更正",
|
||||||
|
"VersionLine": "me-web? {0} · 自由软件(AGPL-3.0)"
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
set -e
|
set -e
|
||||||
. "$(dirname "$0")/sdk.sh"
|
. "$(dirname "$0")/sdk.sh"
|
||||||
dotnet --version
|
dotnet --version
|
||||||
|
dotnet run tools/strings.cs -- check
|
||||||
for project in src/MeWeb.Desktop/MeWeb.Desktop.csproj src/MeWeb.Server/MeWeb.Server.csproj tools/Screenshots/Screenshots.csproj; do
|
for project in src/MeWeb.Desktop/MeWeb.Desktop.csproj src/MeWeb.Server/MeWeb.Server.csproj tools/Screenshots/Screenshots.csproj; do
|
||||||
echo "== $project"
|
echo "== $project"
|
||||||
dotnet build "$project" -c Release -nologo -nodeReuse:false -warnaserror
|
dotnet build "$project" -c Release -nologo -nodeReuse:false -warnaserror
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
const string Cldr = "47.0.0";
|
||||||
|
string[] unLanguages = ["ar", "zh", "en", "fr", "ru", "es"];
|
||||||
|
string[] reviewed = ["en", "it"];
|
||||||
|
string[] officialStatuses = ["official", "de_facto_official"];
|
||||||
|
string[] caselessScripts = ["ka"];
|
||||||
|
var supplied = new Dictionary<string, (string Name, string English, bool Rtl)>
|
||||||
|
{
|
||||||
|
["ay"] = ("Aymar aru", "Aymara", false),
|
||||||
|
["bi"] = ("Bislama", "Bislama", false),
|
||||||
|
["dv"] = ("ދިވެހި", "Dhivehi", true),
|
||||||
|
["fj"] = ("Na Vosa Vakaviti", "Fijian", false),
|
||||||
|
["gil"] = ("Taetae ni Kiribati", "Gilbertese", false),
|
||||||
|
["gn"] = ("Avañe'ẽ", "Guarani", false),
|
||||||
|
["hif"] = ("Fiji Baat", "Fiji Hindi", false),
|
||||||
|
["ho"] = ("Hiri Motu", "Hiri Motu", false),
|
||||||
|
["ht"] = ("Kreyòl ayisyen", "Haitian Creole", false),
|
||||||
|
["mh"] = ("Kajin M̧ajeļ", "Marshallese", false),
|
||||||
|
["ms-Arab"] = ("بهاس ملايو", "Malay (Arabic script)", true),
|
||||||
|
["na"] = ("Dorerin Naoero", "Nauruan", false),
|
||||||
|
["ny"] = ("Chichewa", "Chichewa", false),
|
||||||
|
["pau"] = ("Tekoi ra Belau", "Palauan", false),
|
||||||
|
["sm"] = ("Gagana Samoa", "Samoan", false),
|
||||||
|
["ss"] = ("SiSwati", "Swati", false),
|
||||||
|
["tet"] = ("Tetun", "Tetum", false),
|
||||||
|
["tpi"] = ("Tok Pisin", "Tok Pisin", false),
|
||||||
|
["tvl"] = ("Te Ggana Tuuvalu", "Tuvaluan", false),
|
||||||
|
["wni"] = ("Shindzuani", "Ndzwani Comorian", false),
|
||||||
|
["zdj"] = ("Shingazidja", "Ngazidja Comorian", false),
|
||||||
|
};
|
||||||
|
|
||||||
|
var root = FindRoot();
|
||||||
|
var target = Path.Combine(root, "data", "languages.json");
|
||||||
|
using var http = new HttpClient();
|
||||||
|
http.DefaultRequestHeaders.UserAgent.ParseAdd("meweb-languages/1.0");
|
||||||
|
var baseUrl = $"https://raw.githubusercontent.com/unicode-org/cldr-json/{Cldr}/cldr-json/cldr-core/supplemental";
|
||||||
|
var territories = JsonNode.Parse(await http.GetStringAsync($"{baseUrl}/territoryInfo.json"))["supplemental"]["territoryInfo"].AsObject();
|
||||||
|
var containment = JsonNode.Parse(await http.GetStringAsync($"{baseUrl}/territoryContainment.json"))["supplemental"]["territoryContainment"];
|
||||||
|
var members = containment["UN"]["_contains"].AsArray().Select(n => (string)n).ToHashSet();
|
||||||
|
|
||||||
|
var known = CultureInfo.GetCultures(CultureTypes.NeutralCultures | CultureTypes.SpecificCultures).Select(c => c.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var codes = new SortedSet<string>(unLanguages, StringComparer.Ordinal);
|
||||||
|
var missing = new SortedSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (var (territory, info) in territories)
|
||||||
|
{
|
||||||
|
if (!members.Contains(territory)) continue;
|
||||||
|
var population = info["languagePopulation"]?.AsObject();
|
||||||
|
if (population is null) continue;
|
||||||
|
foreach (var (language, details) in population)
|
||||||
|
{
|
||||||
|
var status = (string)details["_officialStatus"];
|
||||||
|
if (status is null || !officialStatuses.Contains(status)) continue;
|
||||||
|
var code = language.Replace('_', '-');
|
||||||
|
if (known.Contains(code) || supplied.ContainsKey(code)) codes.Add(code);
|
||||||
|
else missing.Add(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var previous = File.Exists(target)
|
||||||
|
? JsonNode.Parse(File.ReadAllText(target)).AsArray().ToDictionary(n => (string)n["code"], n => (string)n["status"])
|
||||||
|
: [];
|
||||||
|
var list = new JsonArray();
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
var (name, english, rtl) = supplied.TryGetValue(code, out var given) ? given : FromCulture(code);
|
||||||
|
var status = reviewed.Contains(code) ? "reviewed" : previous.GetValueOrDefault(code) ?? "draft";
|
||||||
|
list.Add((JsonNode)new JsonObject
|
||||||
|
{
|
||||||
|
["code"] = code,
|
||||||
|
["name"] = name,
|
||||||
|
["english"] = english,
|
||||||
|
["rtl"] = rtl,
|
||||||
|
["status"] = status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
File.WriteAllText(target, list.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }) + "\n");
|
||||||
|
Console.WriteLine($"{list.Count} languages written to data/languages.json (CLDR {Cldr}, {members.Count} UN member states)");
|
||||||
|
if (missing.Count > 0) Console.WriteLine($"official but neither in ICU nor supplied, left out: {string.Join(", ", missing)}");
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
(string Name, string English, bool Rtl) FromCulture(string code)
|
||||||
|
{
|
||||||
|
var culture = CultureInfo.GetCultureInfo(code);
|
||||||
|
var native = culture.NativeName;
|
||||||
|
var name = caselessScripts.Contains(code.Split('-')[0]) || native.Length == 0
|
||||||
|
? native
|
||||||
|
: culture.TextInfo.ToUpper(native[0]) + native[1..];
|
||||||
|
return (name, culture.EnglishName, culture.TextInfo.IsRightToLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
static string FindRoot()
|
||||||
|
{
|
||||||
|
var dir = new DirectoryInfo(Directory.GetCurrentDirectory());
|
||||||
|
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "MeWeb.slnx"))) dir = dir.Parent;
|
||||||
|
return dir?.FullName ?? throw new InvalidOperationException("run from inside the repository (MeWeb.slnx not found)");
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
var root = FindRoot();
|
||||||
|
var folder = Path.Combine(root, "src", "MeWeb.App", "Strings");
|
||||||
|
var options = new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
|
||||||
|
var placeholder = new Regex(@"\{\d+\}");
|
||||||
|
|
||||||
|
if (args.Length >= 1 && args[0] == "check") return Check();
|
||||||
|
if (args.Length == 3 && args[0] == "set") return Set(args[1], args[2]);
|
||||||
|
Console.Error.WriteLine("usage: dotnet run tools/strings.cs -- check | set <Key> <values.json> (values.json maps a language code to its text)");
|
||||||
|
return 2;
|
||||||
|
|
||||||
|
int Check()
|
||||||
|
{
|
||||||
|
var english = Read("en");
|
||||||
|
var problems = 0;
|
||||||
|
foreach (var file in Directory.GetFiles(folder, "*.json").OrderBy(f => f, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
var code = Path.GetFileNameWithoutExtension(file);
|
||||||
|
if (code == "en") continue;
|
||||||
|
var values = Read(code);
|
||||||
|
var missing = english.Keys.Where(k => !values.ContainsKey(k)).ToList();
|
||||||
|
var extra = values.Keys.Where(k => !english.ContainsKey(k)).ToList();
|
||||||
|
var mismatched = english.Keys.Where(values.ContainsKey)
|
||||||
|
.Where(k => !placeholder.Matches(english[k]).Select(m => m.Value).Order().SequenceEqual(placeholder.Matches(values[k]).Select(m => m.Value).Order())).ToList();
|
||||||
|
if (missing.Count + extra.Count + mismatched.Count == 0) continue;
|
||||||
|
problems++;
|
||||||
|
Console.WriteLine($"{code}: missing [{string.Join(", ", missing)}] extra [{string.Join(", ", extra)}] placeholders [{string.Join(", ", mismatched)}]");
|
||||||
|
}
|
||||||
|
Console.WriteLine(problems == 0 ? $"strings: OK ({english.Count} keys)" : $"strings: {problems} language(s) to fix");
|
||||||
|
return problems == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Set(string key, string valuesPath)
|
||||||
|
{
|
||||||
|
var values = JsonNode.Parse(File.ReadAllText(valuesPath)).AsObject();
|
||||||
|
if (values["en"] is null) throw new InvalidOperationException("values.json must carry at least \"en\"");
|
||||||
|
foreach (var (code, value) in values)
|
||||||
|
{
|
||||||
|
var strings = Read(code);
|
||||||
|
strings[key] = (string)value;
|
||||||
|
Write(code, strings);
|
||||||
|
}
|
||||||
|
Console.WriteLine($"{key}: set in {values.Count} language(s)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SortedDictionary<string, string> Read(string code)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(folder, $"{code}.json");
|
||||||
|
if (!File.Exists(path)) return new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var node = JsonNode.Parse(File.ReadAllText(path)).AsObject();
|
||||||
|
return new SortedDictionary<string, string>(node.ToDictionary(p => p.Key, p => (string)p.Value), StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Write(string code, SortedDictionary<string, string> strings)
|
||||||
|
{
|
||||||
|
var node = new JsonObject();
|
||||||
|
foreach (var (key, value) in strings) node[key] = value;
|
||||||
|
File.WriteAllText(Path.Combine(folder, $"{code}.json"), node.ToJsonString(options) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static string FindRoot()
|
||||||
|
{
|
||||||
|
var dir = new DirectoryInfo(Directory.GetCurrentDirectory());
|
||||||
|
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "MeWeb.slnx"))) dir = dir.Parent;
|
||||||
|
return dir?.FullName ?? throw new InvalidOperationException("run from inside the repository (MeWeb.slnx not found)");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user